身在杭州,看着上海垃圾分類如火如荼的進行,心裏難免有些慌亂,爲了更好、更有趣的學習垃圾分類知識,我和小夥伴利用業餘時間開發了一款垃圾分類遊戲,咱們首先肯定了基調,遊戲要有魔性的畫風、粗糙的風格,但粗中有細,簡單有趣,又富有挑戰性,下面是遊戲的預覽圖和視頻。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 下載,歡迎你們下載,祝你們遊戲愉快。