身在杭州,看着上海垃圾分类如火如荼的进行,心里难免有些慌乱,为了更好、更有趣的学习垃圾分类知识,我和小伙伴利用业余时间开发了一款垃圾分类游戏,咱们首先肯定了基调,游戏要有魔性的画风、粗糙的风格,但粗中有细,简单有趣,又富有挑战性,下面是游戏的预览图和视频。node
游戏视频: www.bilibili.com/video/av627…c++
游戏已上架 App Store,打开 App Store 搜索 垃圾分类王 便可下载,或者点击这里直接跳转到 App Store 下载,欢迎你们下载,祝你们游戏愉快。数据库
游戏的核心元素包括小人、垃圾、垃圾桶三部分,人物的左手、右手、头部、左脚和右脚各能容纳一个垃圾并外显,当点击底部区域时投掷相应的垃圾,根据人物和垃圾桶的相对位置判断垃圾投到了桶内仍是地面上。json
根据游戏的核心元素,咱们的人物和垃圾采用了骨骼动画形式,以方便“插拔”到人物身上,垃圾桶采用静态贴图,垃圾与垃圾桶、地面的碰撞检测直接基于坐标计算,使用的框架以下:安全
在使用这些框架的过程当中踩了一些坑,也作了一些总结,本文将从骨骼动画、资源动态化、内存数据保护三个角度介绍。bash
该游戏的核心是那个魔性的小人,提及这我的物,要追溯到笔者的大学时代,那时候对暴漫十分着迷,手绘了不少暴漫的人物和漫画,该游戏的角色就来自于多年前的那张手绘: 网络
笔者首先将图片导入 js,放大数倍后使用毛笔工具描边,随后按照头、躯干、左右手、左右脚进行分割,获得一系列图片: default_tex.png app
随后经过 DragonBones 提供的 PS 插件将他们导入到 DragonBones,按照不一样部分摆放、绑定骨骼,并建立工程: 框架
为了保证人物的头部、双手和双脚都可以“装备”垃圾,这里预留了 5 根骨骼和相应的 Slot,以人物左边的手(即人物右手)为例:dom
这里给手骨 l-hand 添加了一个子骨骼 weaponBoneL 以及插槽 weaponSlotL 来实现动态装载子骨骼,须要注意的是,这里的 weaponSlotL 图片不能为空,不然在运行时动态替换 Slot 时会抛出错误,笔者的作法是使用一张1x1的透明图片占位。
在设计好骨骼后,下一步就是人物的动画了,在该游戏中人物只有站立和行走两个动画。对于行走动画,笔者采用了双腿交叉来模拟移动,同时大臂、小臂和手交替晃动来模拟人保持平衡:
人物站立的动画,仅仅包含了头部和双手的微动:
装备骨骼的制做相对简单,只须要准备装备贴图,并用一根骨骼进行绑定。 性。
对于 RPG 游戏,最好使用一根有长度的骨骼来绑定图片,以便调整贴图与骨骼的相对方向,来保证装备后的视觉效果正确。
网络上关于 DragonBones 动态换装的文章较少,笔者通过查阅大量资料和摸索,总结出了一套较为稳定的换装方法。
第一步是获取装备的插槽,在上文中讲到为人物的右手增长了一个 weaponBoneL 骨骼和对应的 weaponSlotL 插槽,若是要向右手插入装备,只须要获取 weaponSlotL 插槽,下面的代码节选自游戏源码。
Slot* Role::getSlotByName(const std::string &name) {
// _body 是人物的 armatureDisplay 骨骼对象
Slot *s = _body->getArmature()->getSlot(name);
CCAssert(s != nullptr, "the slot is null");
return s;
}
复制代码
获取到 Slot 后,就能够将另一个骨骼的 Armature 插入其中了,代码以下。
// 这里的 slot 即经过上文的 getSlot 获取到的手部 slot,node 即须要插入手部的骨骼对象
void Role::setSlot(dragonBones::Slot *slot, dragonBones::CCArmatureDisplay *node) {
if (slot == nullptr) {
return;
}
slot->setDisplay(node->getArmature(), dragonBones::DisplayType::Armature);
}
复制代码
总结一下,换装分为三步,先准备好人物骨骼 RoleBone 和装备骨骼 EquipmentBone,随后获取 RoleBone 的 Slot,最后将 EquipmentBone 的 Armature 插入其中。
须要注意的是,在卸载装备时,不能直接给 Slot 设置一个空,不然再次插入时会抛出错误,这里的方案也是插入一个透明占位图骨骼。
现代游戏都具备很强的热更新能力,一方面是基于脚本的逻辑动态化能力,另外一方面是基于资源补丁的资源动态化能力,因为开发时间短,笔者与小伙伴在开发游戏时没有接入 Cocos2d-x JSB,采用了纯 C++ 的开发方式,只对资源进行了动态化。
资源动态化有两种方式,其一是使用 Cocos2d-x 提供的 AssetsManager 类,其二是本身实现一套资源补丁系统。因为前者设计的较为复杂,且文档较少,所以笔者采用了自主开发的方式。
为了实现资源的动态化,就要保证全部资源的路径不能写死,而是要采用查表的方式,这是实现资源动态化的关键。
资源路径表由资源描述符 ResourceMapItem 组成,每一个资源描述符包含 namespace、key 和 path 三个部分,结构以下。
class ResourceMapItem {
public:
std::string ns;
std::string key;
std::string path;
static ResourceMapItem* fromValueMap(const std::string &ns, const std::string &key, const cocos2d::ValueMap &vm);
};
复制代码
游戏的全部资源都须要录入到资源路径表,形式以下。
经过加载资源描述符构建出资源路径表,经过 namespace + key 的方式查询实际路径,路径查找经过 R 函数实现,例如查找 local_storage 文件的路径,则使用 R("configs", "stage")
,这里模仿了 Android 对本地资源的管理方式。
虽然索引包含了 namespace 和 key 两部分,可是不必创建一个二级索引表,只须要将 namespace + key 组合出一个惟一索引便可,资源路径表的结构以下。
class ResourceMap {
public:
std::string version;
std::map<std::string, ResourceMapItem *> items;
static ResourceMap* fromValueMap(const cocos2d::ValueMap &vm);
static std::string genKey(const std::string &ns, const std::string &key);
};
复制代码
在加载资源描述符时,首先经过 genKey 方法生成索引,而后存储到 items 这个一级索引表便可,读取配置时,一样经过传入的 namespace 和 key 调用 genKey 方法生成索引,查询 items 表便可,代码以下。
#define R(ns, key) ResourceManager::getInstance()->getResourcePath(ns, key)
std::string ResourceManager::getResourcePath(const std::string &ns, const std::string &key) {
// 生成索引 key
std::string resourceKey = ResourceMap::genKey(ns, key);
if (resourceMap == nullptr) {
CCAssert(false, "resource map is null");
return "";
}
if (resourceMap->items.find(resourceKey) == resourceMap->items.end()) {
CCAssert(false, "cannot find resource, maybe the patch is damaged");
return "";
}
// 查表获取描述符,进而获取到 path
std::string path = resourceMap->items[resourceKey]->path;
// 这里是对形如 ${ConfigsDir} 的路径变量作解析,这是为了处理 iOS 沙盒路径动态生成的问题
RMFileUtil::resolvePath(path);
return path;
}
复制代码
有了资源路径表之后,只须要在启动时选择加载不一样的资源路径表,便可实现资源路径的动态化,为资源动态化打下了基础。
参考 Cocos2d-x 的 AssertManager 设计,一个补丁包含 manifest.json 描述文件和资源列表,结构以下。
首先是目录结构:
随后是描述文件 manifest.json:
{
"version": "patch_192001",
"role": {
"default": {
"rpath": "role"
}
},
"rubbish": {
"config": {
"rpath": "rubbish/rubbish.plist"
},
"milk": {
"rpath": "rubbish"
}
}
}
复制代码
manifest 指明了补丁名称、要 patch 的资源所在路径,这里包含了对角色和对垃圾的 patch。
补丁以压缩包的形式上传到 CDN,在游戏启动时获取当前版本的 patch 列表,patch 列表中会包含当前版本的 patch name 和 path:
{
"patch": {
"version": "1.0",
"patch_list": [
{
"name": "patch_192001",
"url": "http://somecdn.com/patch_192001.zip"
}
]
}
}
复制代码
本地处理的方式为下载、解压,解析 manifest.json,随后根据资源的 key 和 value 对资源路径表进行修改,只要保证补丁资源解压的路径被正确的写入资源路径表,便可实现资源的动态化。
这里有一个细节是对 patch 是否成功的判断,笔者采用的方法是在将新的资源路径写入资源路径表后,在补丁解压的目录放置一个 stub(存根) 文件,此后游戏启动时,根据拉取到的游戏配置中的补丁列表 一一查找本地存根,对于已有存根的直接跳过便可。
// 写入存根
void RubbishGamePatchManager::markPatchAsSuccess(const std::string name) {
std::string path = StringUtils::format("%s/%s/active_stub", RMFileUtil::getPatchesDir().c_str(), name.c_str());
bool success = FileUtils::getInstance()->writeStringToFile("success", path);
if (success) {
SLogInfo("patch %s success", name.c_str());
}
}
// 读取存根
bool RubbishGamePatchManager::hasPatchNamed(const std::string name) {
std::string path = StringUtils::format("%s/%s/active_stub", RMFileUtil::getPatchesDir().c_str(), name.c_str());
return FileUtils::getInstance()->isFileExist(path);
}
复制代码
人不免会犯错,好比下发了一个有问题的补丁,致使游戏 Crash,为了应对这类问题 Crash,咱们能够对资源加载失败和连续 Crash 的状况进行记录,游戏启动时优先检查是否有此类问题,有则删除全部补丁和远程配置,来实现临时的问题止血。
相信不少玩家都据说 CheatEngine 和 八门神器 等内存数据修改神器,他们有一个门槛很低、功能很强大的功能,那就是定位和修改内存中的值,例如某小白玩一款单机 RPG 游戏,他想要修改本身的金币,他能够进行以下的操做:
应对这类状况,通常有两种方式,第一种方式是不信任内存中的值,每次写入数值时,都写入一个非内存的空间,例如数据库或本地文件,读取时也是从非内存的空间读取,这种方式的缺点在于性能很差,不适合频繁的数据操做;第二种方式是对内存中的数据进行加密,笔者很是推荐第二种方式,不只性能优异,并且还能有效的防止小白修改内存。
这里的加密能够采用简单的异或,由于异或有一个很好的特性,异或两次一样的 key 将获得原来的值:
> xorKey
12345
> 2000 ^ xorKey
14313
> 14313 ^ xorKey
2000
复制代码
利用这个特性,只要在安全数字类构造时先随机生成一个 xorKey,而后在每次存入数据时,先异或一下 key 再存入,读取时再异或一下 key,便可简单的实现内存保护,有效防止小白用户修改。
class SecurityNumber {
private:
long memInteger;
public:
SecurityNumber();
~SecurityNumber();
void setInt(int val);
void setLong(long val);
int getInt();
long getLong();
}
复制代码
// 在构造 Number 时随机生成异或 key
SecurityNumber::SecurityNumber() {
key = random();
setLong(0);
}
void SecurityNumber::setInt(int val) {
memInteger = val ^ key;
}
void SecurityNumber::setLong(long val) {
memInteger = val ^ key;
}
int SecurityNumber::getInt() {
return (int)(memInteger ^ key);
}
long SecurityNumber::getLong() {
return memInteger ^ key;
}
复制代码
为了能让使用者像使用普通的数值类型同样无感知的使用 SecurityNumber,能够重载各类运算符,使得 SecurityNumber 能够和 int、long、float 等正常运算。
在这款游戏的开发过程当中,我和个人小伙伴付出了很大心血,也获得了一些成长,如今将这些经验分享给你们,但愿能对你们有所帮助。
咱们的游戏已上架 App Store,打开 App Store 搜索 垃圾分类王 便可下载,或者点击这里直接跳转到 App Store 下载,欢迎你们下载,祝你们游戏愉快。