制作横版游戏KillBear第3课:添加摇杆并控制英雄

2015年03月19日 13:14 0 点赞 0 评论 更新于 2025-11-21 13:30

这个系列今天进入第三课。在第一课中,我们学习了添加地图;第二课则学习了添加英雄人物。在本篇文章中,我们将分两部分进行:上半部分,我们会在控制层 OperateLayer 中加入一个摇杆,并通过该摇杆控制 Hero;下半部分,我们将控制 Hero,防止其跑出地图和跑上墙。

开发环境

  • Win64 : vs2010
  • Cocos2d-x v3.4Final
  • TexturePackerGUI
  • MapEdit

代码构建A

管理Operate

摇杆Joystick

Joystick.h
class Joystick : public Sprite {
public:
Joystick();
~Joystick();
virtual bool init();
virtual void onTouchesBegan(const std::vector<Touch*>& touches, cocos2d::Event *unused_event);
virtual void onTouchesMoved(const std::vector<Touch*>& touches, cocos2d::Event *unused_event);
virtual void onTouchesEnded(const std::vector<Touch*>& touches, cocos2d::Event *unused_event);
void setJoystick(Vec2 point);
CREATE_FUNC(Joystick);
private:
void showJoystick();
void hideJoystick();
void updateJoystick(Touch* touch);
int m_pJoystickr;
int m_pJoystickR;
Sprite *m_pJoystick;
Sprite *m_pJoystickBg;
Vec2 start;
};

Joystick.cpp
bool Joystick::init() {
bool ret = false;
do {
CC_BREAK_IF( !Sprite::init() );
m_pJoystickBg = Sprite::create("JoystickBg.png"); // 背景
m_pJoystick = Sprite::create("Joystick.png"); // 摇杆
this->addChild(m_pJoystickBg, 0);
this->addChild(m_pJoystick, 1);
this->hideJoystick();
m_pJoystickR = m_pJoystickBg->getContentSize().width / 2;
m_pJoystickr = m_pJoystick->getContentSize().width / 2;
// 新的API注册
auto listener = EventListenerTouchAllAtOnce::create();
listener->onTouchesBegan = CC_CALLBACK_2(Joystick::onTouchesBegan, this);
listener->onTouchesMoved = CC_CALLBACK_2(Joystick::onTouchesMoved, this);
listener->onTouchesEnded = CC_CALLBACK_2(Joystick::onTouchesEnded, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
ret = true;
} while(0);
return ret;
}

void Joystick::showJoystick() {
// 显示摇杆
m_pJoystick->setVisible(true);
m_pJoystickBg->setVisible(true);
}

void Joystick::hideJoystick() {
// 隐藏摇杆
m_pJoystick->setVisible(false);
m_pJoystickBg->setVisible(true);
}

void Joystick::onTouchesBegan(const std::vector<Touch*>& touches, Event *unused_event) {
// 按下事件处理
std::vector<Touch*>::const_iterator touchIter = touches.begin();
Touch* touch = *touchIter;
if(m_pJoystick->getBoundingBox().containsPoint(touch->getLocation())) {
this->showJoystick();
updateJoystick(touch);
CCLOG("***************");
CCLOG("update touch:%f \n%f", touch->getLocation().x, touch->getLocation().y);
return;
}
}

void Joystick::onTouchesMoved(const std::vector<Touch*>& touches, Event *unused_event) {
// 移动时处理
std::vector<Touch*>::const_iterator touchIter = touches.begin();
Touch* touch = *touchIter;
if(m_pJoystick->isVisible()) {
updateJoystick(touch);
return;
}
}

void Joystick::onTouchesEnded(const std::vector<Touch*>& touches, Event *unused_event) {
// 离开时处理
this->hideJoystick();
}

void Joystick::setJoystick(Vec2 point) {
// 将这个摇杆放在某个坐标上
start = point;
m_pJoystickBg->setPosition(start);
m_pJoystick->setPosition(m_pJoystickBg->getPosition());
}

void Joystick::updateJoystick(Touch* touch) {
// 更新摇杆状态
// 使用向量来判断
Vec2 hit = touch->getLocation();
float distance = start.getDistance(hit);
Vec2 direction = (hit - start).getNormalized();
// 为了防止摇杆移出摇杆背景
if(distance < m_pJoystickr / 2) {
m_pJoystick->setPosition(start + (direction * distance));
} else if(distance > m_pJoystickr) {
m_pJoystick->setPosition(start + (direction * m_pJoystickr));
} else {
m_pJoystick->setPosition(start + (direction * m_pJoystickr / 2));
}
// global->hero->onMove(direction, distance);
}

这种 Joystick 的实现方式使用了向量,而非笛卡尔坐标(xOy),这样的实现方式更易于理解。该摇杆分为两段:

  • 在某个范围A内,摇杆可以随意移动。
  • 当接触点超出最大范围B(即移出了摇杆背景)时,摇杆将被设定在最大边沿B处。
  • 在范围A和B之间,摇杆会沿着A的边沿放置。

这种设计的最大好处是,可以通过摇杆移动距离控制角色的(走)、(跑)切换,避免了必须按2下才能让角色执行跑动的繁琐操作。

控制层OperateLayer

OperateLayer.h

#include "Joystick.h"

OperateLayer.cpp

// init中
auto m_pjoystick = Joystick::create();
m_pjoystick->setJoystick(Vec2(50, 50));
this->addChild(m_pjoystick);

效果A

至此,摇杆的基本添加已经完成。

接下来,我们面临一个问题:JoystickHero 处于不同的层,如何让这个摇杆控制 Hero 呢?实现的方法有很多,这里我们通过创建另一个全局单例类 Global,并将 JoystickHero “注册” 到 Global 上,通过 Joystick 控制 Global 中的 Hero,从而实现对 Hero 的直接控制。

代码构建B

引入Single.h

唯一实例Single

这是一个优秀的单例实现(从其他代码中借鉴而来)。

Single.h
#ifndef _SINGLETON_H
#define _SINGLETON_H
#include <iostream>
template <typename T>
class Singleton {
public:
// 获取类的唯一实例
static inline T* instance();
// 释放类的唯一实例
void release();
protected:
Singleton() {}
~Singleton() {}
static T* _instance;
};

template <typename T>
inline T* Singleton<T>::instance() {
if(NULL == _instance) {
_instance = new T;
}
return _instance;
}

template <typename T>
void Singleton<T>::release() {
if (!_instance)
return;
delete _instance;
_instance = 0;
}

// cpp文件中需要先声明静态变量
#define DECLARE_SINGLETON_MEMBER(_Ty) \
template <> _Ty* Singleton<_Ty>::_instance = NULL;

#endif // _SINGLETON_H

全局类Global

Global.h

#ifndef _GLOBAL_H_
#define _GLOBAL_H_
#include "cocos2d.h"
USING_NS_CC;
#include "Singleton.h"
#include "GameLayer.h"
#include "OperateLayer.h"
#include "StateLayer.h"
// 需引入以下类,否则在这些类中访问单例对象会报错
class GameLayer;
class OperateLayer;
class StateLayer;
class Hero;
class Enemy;

// 全局单例
class Global : public Singleton<Global> {
public:
Global();
~Global();
GameLayer *gameLayer; // 游戏层
OperateLayer *operateLayer; // 操作层
StateLayer *stateLayer; // 状态层
Hero *hero; // 英雄
__Array *enemies; // 敌人
TMXTiledMap *tileMap; // 地图
Point tilePosFromLocation(Vec2 MovePoint, TMXTiledMap *map = NULL); // 将point转换成地图GID的point
bool tileAllowMove(Vec2 MovePoint);
};

#define global Global::instance()

#endif

Global.cpp

#include "Global.h"
DECLARE_SINGLETON_MEMBER(Global);

Global::Global() {}

Global::~Global() {
CC_SAFE_DELETE(gameLayer);
CC_SAFE_DELETE(operateLayer);
CC_SAFE_DELETE(stateLayer);
CC_SAFE_DELETE(hero);
CC_SAFE_DELETE(enemies);
gameLayer = NULL;
operateLayer = NULL;
stateLayer = NULL;
hero = NULL;
enemies = NULL;
tileMap = NULL;
}

Point Global::tilePosFromLocation(Point MovePoint, TMXTiledMap *map) {
Point point = MovePoint - map->getPosition();
Point pointGID = Vec2::ZERO;
pointGID.x = (int) (point.x / map->getTileSize().width);
pointGID.y = (int) ((map->getMapSize().height * map->getTileSize().height - point.y) / map->getTileSize().height);
return pointGID;
}

bool Global::tileAllowMove(Point MovePoint) {
TMXLayer *floor = global->tileMap->getLayer("Floor");
Point tileGid = tilePosFromLocation(MovePoint, global->tileMap);
auto allowpoint = floor->getTileGIDAt(tileGid);
if(0 == allowpoint) {
return false;
}
return true;
}

在需要使用 Global 的地方进行注册,例如在 GameLayer 中:

GameLayer.h

#include "Global.h"

GameLayer.cpp

GameLayer::GameLayer() {
global->gameLayer = this;
}

OperateLayer::OperateLayer() {
global->operateLayer = this;
}

StateLayer::StateLayer() {
global->stateLayer = this;
}

目前需要添加 Global.h 头文件的类有:MapLayerGameLayerStateLayerOperateLayerHeroJoystickRole 等。

角色Role - Hero

Hero.h

void onMove(Vec2 direction, float distance);
void onStop();
void onAttack(int number);
void updateSelf();

Hero.cpp

void Hero::onMove(Vec2 direction, float distance) { // 移动调用
this->setFlippedX(direction.x < 0 ? true : false);
this->runWalkAction();
Vec2 velocity = direction * (distance < 33 ? 1 : 3);
this->setVelocity(velocity);
}

void Hero::onStop() { // 站立
this->runIdleAction();
this->setVelocity(Vec2::ZERO);
}

void Hero::onAttack(int number) { // 执行攻击
this->runNomalAttackA();
}

void Hero::updateSelf() { // 刷新自己
if(this->getCurrActionState() == ACTION_STATE_WALK) {
Vec2 currentP = this->getPosition(); // 当前坐标
Vec2 expectP = currentP + this->getVelocity(); // 期望坐标
Vec2 actualP = expectP; // 实际坐标
this->setPosition(actualP);
this->setLocalZOrder(Director::getInstance()->getVisibleSize().height - this->getPositionY());
}
}

之后,在 Joystick 中调用 HeroonMove 方法,就可以让 Hero 移动了。

控制Operate

Joystick

去掉 Joystick 中的 onTouchesEnded 方法里不必要的代码,并去掉 updateJoystick 中关于 global->hero 的注释,以实现 JoystickGlobalhero 的控制。

效果B

此时,主角的状态虽然可以切换,但无法移动。经过排查,发现原因是 GameLayer 中没有更新主角的坐标。

GameLayer

GameLayer.h

void update(float dt);
void updateHero(float dt);

GameLayer.cpp

// init中启动默认定时器update
this->scheduleUpdate();

void GameLayer::update(float dt) {
this->updateHero(dt);
}

void GameLayer::updateHero(float dt) {
m_pHero->updateSelf(); // 自更新状态
}

效果C

至此,我们终于实现了标题所描述的效果:添加摇杆并控制 Hero

结语

不过,目前仍然存在一些问题,例如主角能跑到天上、主角移出地图后地图不会移动等。这些问题将在下一篇文章中进行解决。