CocosCreator隻談實戰系列1——成語遊戲編輯器篇
前言
作者從18年4月開始試水微信小遊戲,後面又用休閑小遊戲項目嘗試過國内安卓,頭條小遊戲,facebook等平台。
也是從18年4月第一次使用 cocos creator, 感覺creator的開發體驗不錯,特别是從Unity3D 轉到creator很平滑,
無需看太多說明文檔基本就能上手即用,同時,creator也能滿足休閑遊戲快速産出原型和核心玩法的這一要求。
接下來的一段時間,作者打算将手上的一些項目做成creator系列文章,這些項目每一個核心玩法都有所不同,也使用到了creator引擎的許多方面,希望對creator學習路上的朋友有所幫助。
本篇是系列第一篇,所選項目是今年大火的“成語"類,這個項目打算分兩篇介紹,本篇先說關卡編輯器是如何實作的,下一篇再說遊戲本體實作。 有看官可能得問了,為什麼要先說編輯器?俗話說得好啊 ”工欲善其事必先利其編輯器“, 各位,對于成語這種動則幾千關卡的項目,如果沒有一個可以用起來很友善的編輯器,開發效率就變得很低下了。。 而就實際資料來說:
- 這個關卡編輯器使用了一周進行開發
- 一個策劃人員一周可輕松制作 300 關卡
好了,廢話不多說,下面進入正題。
一,需求分析與設計
”謀定而後動, 知止而有得“
寫代碼最好的狀态是,當開始敲下第一行代碼的時候,子產品怎麼劃分,子產品間怎麼牽線搭橋,全盤皆成竹于胸
我們先來看看成語關卡編輯器的需求點吧:
- 我們應該制作一個編輯區,編輯器是9 X 9的格子布局,共81個格子
- 編輯成語的方式,應該是随心所欲的在格子上刷出成語,想怎麼刷就怎麼刷,這樣生成關卡才快,你
把編輯器傳遞給你的策劃同僚,他要面臨的是生成幾千個關卡。。
- 換成語和删成語。 除了自由刷成語這個基本操作,應該支援對某個成語進行選中,把它換成其它更合适的
成語,或者直接删掉重新編輯。
- 去字功能。 這也是編輯器比較重要的一個功能,因為在遊戲中玩家看到的成語都不是完整的需要填空,而去字
功能就是用來編輯那些字顯示為空格需要讓玩家填空。
一個關卡少說有7,8個成語,如果一個一個的去點就太累了,這裡我們實作了一個‘自動一鍵去字’功能,一鍵去字,如果效果不好,再手動微調即可。
- 其它:關卡儲存/關卡加載/成語詞庫配置讀取
需求整理出來了,下一步就是簡單設計和規劃代碼結構:
- *詞條基本資料*
我們希望用一個類來描述成語詞條的基本資料,請記住,它對應的僅僅
是成語詞庫裡的一條資料,而不是成語對象。但是很明顯,最終它會被一個成語對象所引用。
詞條基本資料所需要的資料結構很簡單:
//file idiomData.js
export default class IdiomData
{
constructor(id, chars, pinyin, note)
{
this.id=id; //資料id
this.chars=chars; //儲存成語的chars,例如"一馬當先"
this.pinyin=pinyin; //成語的注音
this.note=note; //成語的出處和釋義
}
//...
}
2. *成語對象*
我們希望用一個類來描述關卡中編輯出的每一條成語對象
成語對象的成員也很簡單:
- 記錄自己占用了哪些格子(因為後續的操作例如換詞/删詞都會使用這個資料)
- 引用成語詞條資料
- 記錄編輯時,自身的方向(就兩個方向:橫 or 豎)
//file Idiom.js
export const IdiomGridDir={
Unknow:0,
Horizontal:1, // 橫
Vertical:2, //豎
};
export default class Idiom
{
constructor(grids)
{
//占用的格子
this.grids=[];
//引用的詞條資料
this.data=null;
//方向 橫or豎
this.girdDir=IdiomGridDir.Unknow;
for(let i=0;i<grids.length;i )
{
this.pushGrid(grids[i]);
}
}
//...
}
- *格子對象*
我們希望用一個類來描述編輯區每個格子的狀态與行為:
- 該格子是否被使用了
- 該格子上面的成語字元(如果沒被使用,就是” ”)
- 該格子對應的成語對象(格子上需要記錄成語對象,并且要以數組形式記錄,因為存在兩個成語
交叉字格子)
- 該格子是否是被共享的格子
- 該格子是否是被‘去字’的狀态
重點就是以上屬性,當然肯定有一些顯示相關屬性就不一一列出了
//file Grid.js
cc.Class({
extends: cc.Component,
properties: {
//格子ID
gridId:{
default:0,
visible:false
},
//是否是被使用的格子
isUsed:{
default:false,
visible:false
},
},
// LIFE-CYCLE CALLBACKS:
onLoad () {
//其它屬性 ...
//引用的詞條資料
this.data=null;
//使用的成語字元
this.char="";
//格子反向儲存idiom引用
this.idioms=[];
//是否是共享格
this.isShareGrid=false;
//是否是被’去字‘狀态
this.isSpaceGrid=false;
},
- *關卡對象*
我們希望用一個類來描述遊戲關卡對象:
關卡對象組織了格子和成語對象,并且負責對刷成語和換成語/删成語/去字/儲存加載等
是以關卡類會包含更多的資料與行為,事實上大部分代碼也集中在關卡類中
關卡對象主要的資料成員為:
- 一個9X9長度的數組,儲存格子資訊
- 一個用于存放已經完成編輯的成語對象數組
- 一個存放編輯過程中選中格子的數組(便于做一些計算,比如确定刷格子的方向,再比如
刷格子的數量是否已經越界了等)
onLoad () {
this.totalGridsNum=this.gridLineNum * this.gridLineNum;
//格子數組 最大長度9x9=81
this.grids=[];
//儲存已經編輯的成語對象的數組
this.idioms=[];
//緩存編輯過程中滑鼠已選中的格子
this.selectedGrids=[];
關卡對象start() 函數
- 建立9X9的編輯區背景格子
- 注冊滑鼠事件,處理格子刷取邏輯
- 注冊鍵盤事件(主要處理CTRL 左鍵,做精确去字選取處理)
start () {
//建立9X9 編輯區背景格子
this.createBgGrids();
//注冊滑鼠事件
this.registTouchEvent();
//注冊鍵盤事件
this.registKeyEvent();
},
關卡對象主要函數如下,原則上我們将每個編輯功能封裝成一個函數,讓他們各司其職:
registTouchEvent:function () //處理touch事件
registKeyEvent:function() //處理鍵盤事件
selectIdiom:function(idiom) //選中一個成語對象
selectGrid:function(grid) //選中一個格子
changeSelectedIdiom:function() //将選中的成語換成其它成語
deleteSelectedIdiom:function() //删除選中的成語
autoRemoveChar:function() //自動給成語去字
saveLevel:function() //儲存關卡
loadLevel:function() //加載關卡
小結一下,其實成語關卡編輯器核心的類就是上述4個,通過簡單的需求分析,我們作了一個比較清晰的劃分,讓它們各自負責各自的工作。這裡想多提一下,有經驗的老鳥看到這裡肯定會發現一個問題,嚴格說類似 registTouchEvent,registKeyEvent, saveLevel,loadLevel 這樣的行為,不屬于Level對象需要負責的工作,更合理的做法,應該是抽象出一個Editor類,來負責處理事件,儲存加載關卡,串聯編輯流程。确實在大一些的項目,筆者更推薦這樣的設計,本次介紹的項目由于結合開發工期原因,将一些編輯器負責的行為添加到了關卡對象中。
二,編輯流程實作
接下來再簡單介紹下編輯流程實作,由于項目本身偏簡單,就隻抓重點講啦。
*1. 刷詞*
刷詞主要是在 TouchEvent事件中處理,其實如果隻考慮在空白格子刷詞,是非常容易處理的,
這種情況下,程式上隻需要判斷格子選取情況,再從成語庫中取詞填充格子就行了,而格子選取符合要求
的條件無非是:必須是連續選中的4格,格子不能有拐彎,選取範圍不要超過編輯有效範圍。
刷詞稍微複雜一些的情況是某些標明的格子已經被使用了,就出現了多詞共享格的情況,這時候必須把這些字和它
的位置作為附加條件,帶到成語詞庫中進行搜尋。
先看一下刷詞邏輯,在touch_end中判斷是否符合刷詞條件:
this.node.on(cc.Node.EventType.TOUCH_END,(event)=>{
let point=event.touch.getLocation();
// 轉到編輯格子區域的 local position
let localPoint=this.node.convertToNodeSpace(point);
localPoint.y=this.node.height - localPoint.y;
// 做一些是否超出編輯範圍等判斷 。。。
//選取的格子長度為4
if(this.selectedGrids.length===4)
{
//先按格子ID排個序,因為格子可能是從右到左或者從下到上刷的
this.selectedGrids.sort((a,b)=>{
return a.gridId - b.gridId;
});
//排序OK後,4個格子ID取出來
let idx0=this.selectedGrids[0].gridId;
let idx1=this.selectedGrids[1].gridId;
let idx2=this.selectedGrids[2].gridId;
let idx3=this.selectedGrids[3].gridId;
//滿足這個條件,說明是橫向連續4格
if(idx1===idx0 1 && idx2===idx1 1 && idx3===idx2 1)
{
//在選取範圍中擷取被共用的char
let shareChars=this.getShareCharsInSelection();
//成語庫中查找
let idiomData=WordsLib.instance.findIdiomData(shareChars);
if(idiomData!==null)
{
//找到了,生成新idiom對象
let idiom=new Idiom(this.selectedGrids);
//新成語儲存關卡中
this.idioms.push(idiom);
this.updateIdiomNumLabel();
//更新成語對象資料
idiom.setIdiomData(idiomData);
//更新格子狀态
idiom.updateGrids();
}
else
{
//沒有合适的成語可填,做一些編輯狀态的清理工作
}
}
//滿足這個條件,說明是縱向連續4格
else if(idx1===idx0 this.gridLineNum && idx2==idx1 this.gridLineNum && idx3===idx2 this.gridLineNum)
{
//内部邏輯和上面橫向是一樣的。。。
}
}
*2. 自動去字*
實作自動去字主要是為編輯時提供一個快捷功能,一個成語最多自動去掉兩個字
寫了一個removeChar函數來處理
removeChar()
{
let ret=this.getSpaceGrids();
//已經有兩個空格字的話就不繼續處理了
if(ret.length===2)
return;
let removeIdx=[0,1,2,3];
if(ret.length===1)
{
//在剩餘的3格裡面再去掉一個字
removeIdx.splice(ret[0],1);
let randIdx=Util.randRangeNumber(0,removeIdx.length-1);
let gid=removeIdx[randIdx];
this.grids[gid].forceSetSpace();
}
else if(ret.length===0)
{
//這是需要去掉兩個字的情況
let randIdx=Util.randRangeNumber(0,removeIdx.length-1);
this.grids[randIdx].forceSetSpace();
removeIdx.splice(randIdx, 1);
randIdx=Util.randRangeNumber(0,removeIdx.length-1);
let gid=removeIdx[randIdx];
this.grids[gid].forceSetSpace();
}
}
以上是關于刷詞和去字的實作,至于說其它如選詞/換詞/删詞/儲存加載,其實實作都很簡單,成語項目整體來說
很容易實作,這些功能就不啰嗦了。
順便作一下下個項目的預告吧,下個項目準備介紹一個豎版跑酷類遊戲——《峭壁逃亡》
該遊戲參與了頭條小遊戲平台的内測,相關新聞連接配接 http://dy.163.com/v2/article/detail/DT5A07F10546236I.html
這個 項目将重點給大家介紹 跑酷類遊戲無限關卡的生成方法,以及基于dragonBone的角色控制, 敬請期待。
筆者簡介:
肖堯,從事遊戲 前端/後端/3D引擎開發多年
前盛大錦天項目主程,前成都網龍研發負責人,進階架構師
現任休閑遊戲公司H5技術總監,未來将持續專注于基于H5的泛娛樂/教育/傳媒/工具等産品的研究與開發。
微信/QQ : 1611471