在RPG游戏中,有着各种各样的NPC(Non-Player Control),玩家可以操作主角与NPC进行交互,来获得情报,道具,装备等等。而NPC的概念比较广泛,从各种商人到宝箱再到空气墙,都可以认为是NPC,只不过它们的贴图和响应不同。
主角的动画一般情况下是最丰富的,如行走图,战斗图,立绘等等。而RPG游戏角色的数量很多,因此需要一个扩展性强,处理便捷的格式进行存储和读取,这里采用plist格式来映射,plist格式常用于IOS,它在xml的基础上限定了特定的标签,如<key>键,<string>字符串,<array>数组<dict> 字典等。下面为具体的文本:
<!--角色1-->
<key>sky</key>
<dict>
<!--默认立绘-->
<key>face</key>
<dict>
<key>filename</key>
<string>img/faces/Actor1.png</string>
<key>index</key>
<integer>0</integer>
</dict>
<!--行走图-->
<key>walk</key>
<dict>
<key>filename</key>
<string>img/characters/Actor1.png</string>
<key>delay</key>
<real>0.15f</real>
<key>loops</key>
<integer>-1</integer>
<key>restoreOriginalFrame</key>
<false/>
<key>index</key>
<integer>0</integer>
</dict>
<!--战斗图sv-->
<key>sv</key>
<string>img/sv_actors/Actor1_1.png</string>
<!--turn图-->
<key>turn_name</key>
<string>Actor01_turn.png</string>
<!--升级文件-->
<key>level_up_filename</key>
<string>data/sky.csv</string>
</dict>
在上面的文本中,角色sky有行走图,战斗图,turn图(在战斗时显示出手顺序的图片),以及升级文件。此文本在程序中使用Value进行保存,当需要获取对应的数据时,只需要按照对应的方式进行读取即可。而解析的部分,则交给了StaticData类。StaticData增加了以下几个类:
//加载角色数据以及加载所需要的图片并解析
bool loadCharacterFile(const string& filename);
//获取人物行走动画
Animation* getWalkingAnimation(const string& chartletName, Direction direction);
Animation* getWalkingAnimation(const string& filename, int index, Direction dir, float delay, int loops, bool restoreOriginalFrame);
private:
//添加角色战斗图并生成16状态动画
bool addSVAnimation(const string& filename);
//添加角色升级文件
bool addLevelUpData(const string& filename);
/*在纹理指定区域rect按照宽度切割,并返回*/
vector<SpriteFrame*> splitTexture(Texture* texture, const Rect& rect ,float width);
loadCharacterFile的实现如下
bool StaticData::loadCharacterFile(const string& filename)
{
//获取键值对
m_characterMap = FileUtils::getInstance()->getValueMapFromFile(filename);
//遍历
for (auto it1 = m_characterMap.begin(); it1 != m_characterMap.end(); it1++)
{
//角色名
auto& chartletName = it1->first;
auto& valueMap = it1->second.asValueMap();
//角色相关 如行走图 战斗图等 以下属性全部可选
for (auto value : valueMap)
{
auto key = value.first;
//加载对应资源
if (key == "face" || key == "walk")
{
auto& map = value.second.asValueMap();
auto& filename = map.at("filename").asString();
//加载并返回
Director::getInstance()->getTextureCache()->addImage(filename);
}
else if (key == "sv")
{
string filename = value.second.asString();
this->addSVAnimation(filename);
}
else if (key == "level_up_filename")
{
string filename = value.second.asString();
this->addLevelUpData(filename);
}
}
}
return true;
}
这个函数在读取了对应的数据并保存后,之后便进行了解析,如加载立绘和行走图,而目前的addSVAnimation函数和addLevelUpData函数因为暂时用不到,为空。
vector<SpriteFrame*> StaticData::splitTexture(Texture* texture, const Rect& rect,float width)
{
int col = int (rect.size.width / width);
//进行切割
vector<SpriteFrame*> frames;
SpriteFrame* frame = nullptr;
for (int j = 0;j < col; j++)
{
Rect r(rect.origin, Size(width, rect.size.height));
r.origin.x += j * width;
//r.origin.y += row * size.height;
frame = SpriteFrame::createWithTexture(texture,r);
frames.push_back(frame);
}
return frames;
}
spliteTexture函数的功能是切割指定区域的纹理,生成并返回数组,目前的spiltTexture仅仅分割了一行若干列的图片。
Animation* StaticData::getWalkingAnimation(const string& chartletName, Direction direction)
{
//获取角色map
auto it = m_characterMap.find(chartletName);
if (it == m_characterMap.end())
return nullptr;
auto& valueMap = it->second.asValueMap();
auto& walkMap = valueMap.at("walk").asValueMap();
//获取文件名 等各种属性
auto filename = walkMap.at("filename").asString();
int index = walkMap.at("index").asInt();
float delay = walkMap.at("delay").asFloat();
int loops = walkMap.at("loops").asInt();
bool restoreOriginalFrame = walkMap.at("restoreOriginalFrame").asBool();
return this->getWalkingAnimation(filename,index,direction,delay,loops,restoreOriginalFrame);
}
Animation* StaticData::getWalkingAnimation(const string& filename, int index, Direction dir, float delay, int loops, bool restoreOriginalFrame)
{
auto texture = Director::getInstance()->getTextureCache()->getTextureForKey(filename);
if (texture == nullptr)
return nullptr;
int row = index / 4;
int col = index % 4;
//获取偏移位置
Point offset;
Size size(48.f, 48.f);
offset.x = col * 3 * size.width;
offset.y = (row * 4 + static_cast<int>(dir)) * size.height;
Rect rect(offset, Size(48 * 3, 48.f));
//进行切割
auto frames = this->splitTexture(texture, rect, 48.f);
auto animation = Animation::createWithSpriteFrames(frames, delay, loops);
animation->setRestoreOriginalFrame(restoreOriginalFrame);
return animation;
}
getWalkingAnimation则是在每次调用都会根据角色名称和方向生成并返回一个新的行走动画。而不保存到AnimationCache的原因是有的角色并不都是有四方向的行走图。
接下来创建Entity和Character类。
#ifndef __Entity_H__
#define __Entity_H__
#include<string>
#include "SDL_Engine/SDL_Engine.h"
using namespace SDL;
using namespace std;
class Entity:public Node
{
SDL_BOOL_SYNTHESIZE(m_bOpenShade,OpenShade);//是否开启遮罩 可删除
public:
static const int ANIMATION_TAG;
protected:
Sprite*m_pSprite;
public:
Entity();
~Entity();
Sprite*getSprite()const;
//和bind不同,此函数不改变content size
void setSprite(Sprite*sprite);
void bindSprite(Sprite*sprite);
Sprite* bindSpriteWithSpriteFrame(SpriteFrame*spriteFrame);
Sprite* bindSpriteWithSpriteFrameName(const string&spriteName);
//以animation 的第一帧为贴图 并且运行该动画
Sprite* bindSpriteWithAnimate(Animate*animate);
//创建动画
static Animate*createAnimate(std::string format,int begin,int end,float delayPerUnit,unsigned int loops=-1);
};
#endif
可删除
public:
static const int ANIMATION_TAG;
protected:
Sprite*m_pSprite;
public:
Entity();
~Entity();
Sprite*getSprite()const;
//和bind不同,此函数不改变content size
void setSprite(Sprite*sprite);
void bindSprite(Sprite*sprite);
Sprite* bindSpriteWithSpriteFrame(SpriteFrame*spriteFrame);
Sprite* bindSpriteWithSpriteFrameName(const string&spriteName);
//以animation 的第一帧为贴图 并且运行该动画
Sprite* bindSpriteWithAnimate(Animate*animate);
//创建动画
static Animate*createAnimate(std::string format,int begin,int end,float delayPerUnit,unsigned int loops=-1);
};
#endif
Entity类采用组合的方式来使得动画和动作分开,各司其职。对于内部的精灵来说,它的职责就是运行各种动画,如行走动画。而Entity则进行各种动作,如MoveTo等等。
#include "Entity.h"
const int Entity::ANIMATION_TAG = 100;
Entity::Entity()
:m_pSprite(nullptr)
,m_bOpenShade(false)
{
}
Entity::~Entity()
{
}
void Entity::setSprite(Sprite*sprite)
{
if(m_pSprite)
m_pSprite->removeFromParent();
m_pSprite = sprite;
Size size = this->getContentSize();
m_pSprite->setPosition(size.width/2,size.height/2);
this->addChild(m_pSprite);
}
void Entity::bindSprite(Sprite*sprite)
{
if(m_pSprite)
m_pSprite->removeFromParent();
m_pSprite = sprite;
auto size = m_pSprite->getContentSize();
this->setContentSize(size);
m_pSprite->setPosition(size.width/2,size.height/2);
this->addChild(m_pSprite);
}
Sprite* Entity::bindSpriteWithSpriteFrame(SpriteFrame*spriteFrame)
{
if(spriteFrame != nullptr)
{
Sprite*sprite = Sprite::createWithSpriteFrame(spriteFrame);
Entity::bindSprite(sprite);
return sprite;
}
return nullptr;
}
Sprite* Entity::bindSpriteWithSpriteFrameName(const string&spriteName)
{
//获取精灵帧
auto spriteFrame = Director::getInstance()->getSpriteFrameCache()->getSpriteFrameByName(spriteName);
return this->bindSpriteWithSpriteFrame(spriteFrame);
}
Sprite*Entity::bindSpriteWithAnimate(Animate*animate)
{
auto animation = animate->getAnimation();
auto firstFrame = animation->getFrames().front()->getSpriteFrame();
auto sprite = this->bindSpriteWithSpriteFrame(firstFrame);
//运行动画
sprite->runAction(animate);
return sprite;
}
Sprite*Entity::getSprite()const
{
return m_pSprite;
}
Animate*Entity::createAnimate(std::string format,int begin,int end,float delayPerUnit,unsigned int loops)
{
std::vector<SpriteFrame*> frames;
auto spriteFrameCache = Director::getInstance()->getSpriteFrameCache();
//添加资源
//spriteFrameCache->addSpriteFramesWithFile(xml,png);
for(int i=begin;i<=end;i++)
{
auto frame = spriteFrameCache->getSpriteFrameByName(StringUtils::format(format.c_str(),i));
frames.push_back(frame);
}
Animation*animation = Animation::createWithSpriteFrames(frames,delayPerUnit,loops);
return Animate::create(animation);
}
一般情况下,很少会直接用到Entity对象,而较常用到的是Entity的派生类。
#ifndef __Character_H__
#define __Character_H__
#include <string>
#include "Entity.h"
using namespace std;
enum class State
{
None,
Idle,
Walking,
};
enum class Direction;
class Character : public Entity
{
SDL_SYNTHESIZE_READONLY(string, m_chartletName, ChartletName);//当前贴图名,也可以认为是人物名称,唯一
SDL_SYNTHESIZE(float, m_durationPerGrid, DurationPerGrid);//每格的行走时间
private:
Direction m_dir;
State m_state;
bool m_bDirty;
Character* m_pFollowCharacter;
public:
Character();
~Character();
static Character* create(const string& chartletName);
bool init(const string& chartletName);
//跟随某角色
void follow(Character* character);
//方向改变
Direction getDirection() const;
void setDirection(Direction direction);
private:
//获取方向所对应的动画
Animation* getWalkingAnimation(Direction dir);
//切换状态
void changeState(State state);
};
#endif
目前的角色类的功能较少,仅仅是运行动画,切换状态而已。
#include "Character.h"
#include "StaticData.h"
Character::Character()
:m_durationPerGrid(0.3f)
,m_dir(Direction::Down)
,m_state(State::None)
,m_bDirty(true)
,m_pFollowCharacter(nullptr)
{
}
Character::~Character()
{
}
Character* Character::create(const string& chartletName)
{
auto character = new Character();
if (character && character->init(chartletName))
character->autorelease();
else
SDL_SAFE_DELETE(character);
return character;
}
bool Character::init(const string& chartletName)
{
m_chartletName = chartletName;
//设置当前为站立状态
this->changeState(State::Idle);
return true;
}
void Character::follow(Character* character)
{
character->m_pFollowCharacter = this;
this->setPosition(character->getPosition());
}
Direction Character::getDirection() const
{
return m_dir;
}
void Character::setDirection(Direction direction)
{
if (m_dir != direction)
{
m_dir = direction;
m_bDirty = true;
this->changeState(m_state);
}
}
Animation* Character::getWalkingAnimation(Direction dir)
{
auto animation = StaticData::getInstance()->getWalkingAnimation(m_chartletName,dir);
return animation;
}
void Character::changeState(State state)
{
if (m_state == state && !m_bDirty)
return;
m_state = state;
m_bDirty = false;
Animation* animation = nullptr;
switch (state)
{
case State::None:
break;
case State::Walking:
{
animation = this->getWalkingAnimation(m_dir);
}break;
case State::Idle://人物站立图跟素材相关
{
animation = this->getWalkingAnimation(m_dir);
auto frame = animation->getFrames().at(1)->getSpriteFrame();
if (m_pSprite == nullptr)
this->bindSpriteWithSpriteFrame(frame);
else
m_pSprite->setSpriteFrame(frame);
animation = nullptr;
}
break;
default:
break;
}
if (animation == nullptr)
return;
//运行动画
auto animate = Animate::create(animation);
animate->setTag(ANIMATION_TAG);
//this->getSprite()->runAction(animate);
if (m_pSprite == nullptr)
{
this->bindSpriteWithAnimate(animate);
}
else
{
m_pSprite->stopActionByTag(ANIMATION_TAG);
m_pSprite->runAction(animate);
}
}
changeState函数的功能是在切换状态的同时更换对应的动画,而如果角色的方向改变也会导致动画发生变化。角色类是可以设置跟随的。
PlayerLayer负责管理主角团队,但实际上PlayerLayer只是包含主角的引用,这个稍后会提到。
#ifndef __PlayerLayer_H__
#define __PlayerLayer_H__
#include<vector>
#include "SDL_Engine/SDL_Engine.h"
using namespace std;
using namespace SDL;
class Character;
class PlayerLayer : public Layer
{
private:
vector<Character*> m_characters;
public:
PlayerLayer();
~PlayerLayer();
CREATE_FUNC(PlayerLayer);
bool init();
virtual void update(float dt);
//获取主角 默认第一个为主角
Character* getPlayer() const;
//获取角色列表
vector<Character*>& getCharacterList();
//获取角色对应的索引
int getIndexOfCharacter(const string& chartletName);
Character* getPlayerOfId(int id);
//添加角色
void addCharacter(Character* character);
//是否与角色发生碰撞
bool isCollidedWithCharacter(const Rect& rect);
};
#endif
#include "PlayerLayer.h"
#include "Character.h"
PlayerLayer::PlayerLayer()
{
}
PlayerLayer::~PlayerLayer()
{
for (auto it = m_characters.begin();it != m_characters.end();)
{
auto player = *it;
SDL_SAFE_RELEASE_NULL(player);
it = m_characters.erase(it);
}
}
bool PlayerLayer::init()
{
return true;
}
void PlayerLayer::update(float dt)
{
}
Character* PlayerLayer::getPlayer()const
{
return m_characters.front();
}
vector<Character*>& PlayerLayer::getCharacterList()
{
return m_characters;
}
int PlayerLayer::getIndexOfCharacter(const string& chartletName)
{
int i = -1;
for (i = 0;i < m_characters.size();i++)
{
auto player = m_characters.at(i);
if (player->getChartletName() == chartletName)
break;
}
return i;
}
Character* PlayerLayer::getPlayerOfId(int id)
{
//遍历寻找
for (int i = 0;i < m_characters.size();i++)
{
auto player = m_characters.at(i);
if (player->getUniqueID() == id)
return player;
}
return nullptr;
}
void PlayerLayer::addCharacter(Character* character)
{
m_characters.push_back(character);
SDL_SAFE_RETAIN(character);
}
bool PlayerLayer::isCollidedWithCharacter(const Rect& rect)
{
for (int i = 0;i < m_characters.size();i++)
{
auto r = m_characters.at(i)->getBoundingBox();
if (r.intersectRect(rect))
return true;
}
return false;
}
角色层里有一个数组来保存角色指针并添加引用。
然后就需要在GameScene中创建并添加角色到场景中去。
#ifndef __GameScene_H__
#define __GameScene_H__
#include <string>
#include "SDL_Engine/SDL_Engine.h"
using namespace std;
USING_NS_SDL;
class MapLayer;
class PlayerLayer;
class GameScene : public Scene
{
private:
static GameScene* s_pInstance;
public:
static GameScene* getInstance();
static void purge();
private:
MapLayer* m_pMapLayer;
PlayerLayer* m_pPlayerLayer;
public:
static const int CHARACTER_LOCAL_Z_ORDER = 9999;//需要比tmx地图总图块大
private:
GameScene();
~GameScene();
bool init();
void preloadResources();
bool initializeMapAndPlayers();
public:
//改变场景
void changeMap(const string& mapName, const Point& tileCoodinate);
//设置视图中心点
void setViewPointCenter(const Point& position, float duration = 0.f);
};
#endif
class PlayerLayer;
class GameScene : public Scene
{
private:
static GameScene* s_pInstance;
public:
static GameScene* getInstance();
static void purge();
private:
MapLayer* m_pMapLayer;
PlayerLayer* m_pPlayerLayer;
public:
static const int CHARACTER_LOCAL_Z_ORDER = 9999;//需要比tmx地图总图块大
private:
GameScene();
~GameScene();
bool init();
void preloadResources();
bool initializeMapAndPlayers();
public:
//改变场景
void changeMap(const string& mapName, const Point& tileCoodinate);
//设置视图中心点
void setViewPointCenter(const Point& position, float duration = 0.f);
};
#endif
注:红色的为新加的,紫色的重大修改过的。
bool GameScene::init()
{
this->preloadResources();
//地图层
m_pMapLayer = MapLayer::create();
this->addChild(m_pMapLayer);
//角色层
m_pPlayerLayer = PlayerLayer::create();
this->addChild(m_pPlayerLayer);
//初始化地图和角色
this->initializeMapAndPlayers();
return true;
}
this->preloadResources();
//地图层
m_pMapLayer = MapLayer::create();
this->addChild(m_pMapLayer);
//角色层
m_pPlayerLayer = PlayerLayer::create();
this->addChild(m_pPlayerLayer);
//初始化地图和角色
this->initializeMapAndPlayers();
return true;
}
GameScene类在初始化函数中,预加载了资源,并且创建了各种层。
void GameScene::preloadResources()
{
StaticData::getInstance()->loadCharacterFile("data/character.plist");
}
bool GameScene::initializeMapAndPlayers()
{
//获取地图
auto dynamicData = DynamicData::getInstance();
//TODO:暂时使用存档1
dynamicData->initializeSaveData(1);
//获得存档玩家控制的主角队伍的数据
auto& valueMap = dynamicData->getTotalValueMapOfCharacter();
Character* last = nullptr;
//解析数据并生成角色
for (auto itMap = valueMap.begin();itMap != valueMap.end();itMap++)
{
auto chartletName = itMap->first;
auto& propertiesMap = itMap->second.asValueMap();
//创建角色
Character* player = Character::create(chartletName);
player->setDurationPerGrid(0.25f);
//传递给主角层
m_pPlayerLayer->addCharacter(player);
//TODO:设置属性
//DynamicData::getInstance()->
//设置跟随
if (last != nullptr)
player->follow(last);
else//设置视角跟随
{
//this->setViewpointFollow(player);
}
last = player;
}
auto mapFilePath = dynamicData->getMapFilePath();
auto tileCooridinate = dynamicData->getTileCoordinateOfPlayer();
//改变地图
this->changeMap(mapFilePath, tileCooridinate);
return true;
}
在上面的函数中,根据存档生成对应的角色,并设置跟随。在以后添加了角色的属性后,还会对角色的属性进行解析并赋值。
void GameScene::changeMap(const string& mapName, const Point &tileCoodinate)
{
//获取主角列表 并清除
auto &characterList = m_pPlayerLayer->getCharacterList();
for (auto character : characterList)
{
character->retain();
//character->sit();
//character->clear();
character->removeFromParent();
}
//改变当前地图
m_pMapLayer->clear();
m_pMapLayer->init(mapName);
//获取碰撞层
auto collisionLayer = m_pMapLayer->getCollisionLayer();
//设置主角位置
auto tileSize = m_pMapLayer->getTiledMap()->getTileSize();
auto pos = Point(tileSize.width * (tileCoodinate.x + 0.5f)
,tileSize.height * (tileCoodinate.y + 0.5f));
//添加主角
for (unsigned int i = 0; i < characterList.size();i++)
{
auto character = characterList.at(i);
//添加主角,并设置localZOrder
collisionLayer->addChild(character,CHARACTER_LOCAL_Z_ORDER - i);
character->setPosition(pos);
character->release();
}
//改变当前中心点
this->setViewPointCenter(pos);
}
changeMap函数目前的功能除了切换显示的地图外,还会把主角放到新的地图上。需要注意的是,主角并没有添加到PlayerLayer层中,而是添加到了MapLayer类的m_pTiledMap的碰撞层,这样做的好处就是可以很方便地进行遮挡处理(虽然globalZOrder也可以做到,但是对globalZOrder的改变可能会发生意外的遮挡,不建议使用)。添加主角时还改变了主角的localZOrder,以处理主角之见的遮挡关系。
ValueMap& DynamicData::getTotalValueMapOfCharacter()
{
return m_valueMap.at("character").asValueMap();
}
本节运行界面如下:
可以看到右下角,男主在床上睁着眼睛在床上睡觉。。。
本节代码:链接:https://pan.baidu.com/s/130f9r2KRsa38N2SHKVFulQ 密码:rpa9