笔者简介:肖尧,从事游戏前端/后端/3D引擎开发多年
前盛大锦天项目主程,前成都网龙研发负责人,高级架构师
现任休闲游戏公司H5技术总监
未来将持续专注于基于H5的泛娱乐/教育/传媒/工具等产品的研究与开发。
微信/QQ :1611471
CocosCreator只谈实战系列(1)—成语游戏编辑器
前言
作者从18年4月开始试水微信小游戏,后面又用休闲小游戏项目尝试过国内安卓、头条小游戏、facebook等平台。
也是从18年4月第一次使用 Cocos Creator,感觉 Creator 的开发体验不错。特别是从Unity3D 转到 Creator 很平滑,无需看太多说明文档基本就能上手即用。同时,Creator也能满足休闲游戏快速产出原型和核心玩法的这一要求。
接下来的一段时间,作者打算将手上的一些项目做成 Creator 系列文章。这些项目每一个核心玩法都有所不同,也使用到了 Creator 引擎的许多方面,希望对 Creator 学习路上的朋友有所帮助。
本篇是系列第一篇,所选项目是今年大火的“成语"类游戏。
这个项目打算分两篇介绍,本篇先说关卡编辑器是如何实现的,下一篇再说游戏本体实现。
有看官可能得问了,为什么要先说编辑器?俗话说得好啊 “工欲善其事必先利其编辑器”。各位,对于成语这种动则几千关卡的项目,如果没有一个可以用起来很方便的编辑器,开发效率就变得很低下了...而就实际数据来说:
这个关卡编辑器使用了一周进行开发
一个策划人员一周可轻松制作 300+ 关卡
一、需求分析与设计
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/0f0409c15c5d0c1e287ea41c9770b1f0.png)
我们应该制作一个编辑区,编辑器是9 X 9的格子布局,共81个格子
编辑成语的方式,应该是随心所欲的在格子上刷出成语,想怎么刷就怎么刷,这样生成关卡才快,你把编辑器交付给你的策划同事,他要面临的是生成几千个关卡。
换成语和删成语,除了自由刷成语这个基本操作,应该支持对某个成语进行选中,把它换成其它更合适的成语,或者直接删掉重新编辑。
去字功能,这也是编辑器比较重要的一个功能,因为在游戏中玩家看到的成语都不是完整的需要填空,而去字功能就是用来编辑那些字显示为空格需要让玩家填空。
一个关卡少说有7,8个成语,如果一个一个的去点就太累了,这里我们实现了一个‘自动一键去字’功能,一键去字,如果效果不好,再手动微调即可。
其它,关卡保存/关卡加载/成语词库配置读取
//file idiomData.js
export default classIdiomData{
constructor(id, chars, pinyin, note)
{
this.id=id; //数据id
this.chars=chars; //保存成语的chars,例如"一马当先"
this.pinyin=pinyin; //成语的注音
this.note=note; //成语的出处和释义
}
//...
}
-
记录自己占用了哪些格子(因为后续的操作例如换词/删词都会使用这个数据)
-
引用成语词条数据
-
记录编辑时,自身的方向(就两个方向:横 or 竖)
//file Idiom.js export const IdiomGridDir={ Unknow:0, Horizontal:1, // 横 Vertical:2, //竖 }; export default classIdiom{ 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=[];
}
创建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() //加载关卡
二、编辑流程实现
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) {
//内部逻辑和上面横向是一样的。。。
}
}
})
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/e7eab428eaddfa4982c899260e7878a0.jpeg)
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();
}
}