作者从18年4月开始试水微信小游戏,后面又用休闲小游戏项目尝试过国内安卓,头条小游戏,facebook等平台。
也是从18年4月第一次使用 cocos creator, 感觉creator的开发体验不错,特别是从Unity3D 转到creator很平滑,
无需看太多说明文档基本就能上手即用,同时,creator也能满足休闲游戏快速产出原型和核心玩法的这一要求。
接下来的一段时间,作者打算将手上的一些项目做成creator系列文章,这些项目每一个核心玩法都有所不同,也使用到了creator引擎的许多方面,希望对creator学习路上的朋友有所帮助。
本篇是系列第一篇,所选项目是今年大火的“成语"类,这个项目打算分两篇介绍,本篇先说关卡编辑器是如何实现的,下一篇再说游戏本体实现。 有看官可能得问了,为什么要先说编辑器?俗话说得好啊 ”工欲善其事必先利其编辑器“, 各位,对于成语这种动则几千关卡的项目,如果没有一个可以用起来很方便的编辑器,开发效率就变得很低下了。。 而就实际数据来说:
好了,废话不多说,下面进入正题。
”谋定而后动, 知止而有得“
写代码最好的状态是,当开始敲下第一行代码的时候,模块怎么划分,模块间怎么牵线搭桥,全盘皆成竹于胸
我们先来看看成语关卡编辑器的需求点吧:
把编辑器交付给你的策划同事,他要面临的是生成几千个关卡。。
成语,或者直接删掉重新编辑。
功能就是用来编辑那些字显示为空格需要让玩家填空。
一个关卡少说有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. *成语对象*
我们希望用一个类来描述关卡中编辑出的每一条成语对象
成语对象的成员也很简单:
//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
我们希望用一个类来描述编辑区每个格子的状态与行为:
交叉字格子)
重点就是以上属性,当然肯定有一些显示相关属性就不一一列出了
//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;
},
我们希望用一个类来描述游戏关卡对象:
关卡对象组织了格子和成语对象,并且负责对刷成语和换成语/删成语/去字/保存加载等
因此关卡类会包含更多的数据与行为,事实上大部分代码也集中在关卡类中
关卡对象主要的数据成员为:
刷格子的数量是否已经越界了等)
onLoad () {
this.totalGridsNum=this.gridLineNum * this.gridLineNum;
//格子数组 最大长度9x9=81
this.grids=[];
//保存已经编辑的成语对象的数组
this.idioms=[];
//缓存编辑过程中鼠标已选中的格子
this.selectedGrids=[];
关卡对象start() 函数
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