大神驾到 | 盛大锦天大神,做客Creator星球

笔者简介:肖尧,从事游戏前端/后端/3D引擎开发多年

前盛大锦天项目主程,前成都网龙研发负责人,高级架构师

现任休闲游戏公司H5技术总监

未来将持续专注于基于H5的泛娱乐/教育/传媒/工具等产品的研究与开发。

微信/QQ :1611471


640?wx_fmt=gif

CocosCreator只谈实战系列(1)—成语游戏编辑器
前言

作者从18年4月开始试水微信小游戏,后面又用休闲小游戏项目尝试过国内安卓、头条小游戏、facebook等平台。

也是从18年4月第一次使用 Cocos Creator,感觉 Creator 的开发体验不错。特别是从Unity3D 转到 Creator 很平滑,无需看太多说明文档基本就能上手即用。同时,Creator也能满足休闲游戏快速产出原型和核心玩法的这一要求。

接下来的一段时间,作者打算将手上的一些项目做成 Creator 系列文章。这些项目每一个核心玩法都有所不同,也使用到了 Creator 引擎的许多方面,希望对 Creator 学习路上的朋友有所帮助。

本篇是系列第一篇,所选项目是今年大火的“成语"类游戏。

这个项目打算分两篇介绍,本篇先说关卡编辑器是如何实现的,下一篇再说游戏本体实现。

有看官可能得问了,为什么要先说编辑器?俗话说得好啊 “工欲善其事必先利其编辑器”。各位,对于成语这种动则几千关卡的项目,如果没有一个可以用起来很方便的编辑器,开发效率就变得很低下了...而就实际数据来说:

  1. 这个关卡编辑器使用了一周进行开发

  2. 一个策划人员一周可轻松制作 300+ 关卡

好了,废话不多说,下面进入正题。
一、需求分析与设计
“谋定而后动, 知止而有得 ”
写代码最好的状态是,当开始敲下第一行代码的时候,模块怎么划分,模块间怎么牵线搭桥,全盘皆成竹于胸。
640?wx_fmt=png
我们先来看看成语关卡编辑器的需求点吧:
  • 我们应该制作一个编辑区,编辑器是9 X 9的格子布局,共81个格子

  • 编辑成语的方式,应该是随心所欲的在格子上刷出成语,想怎么刷就怎么刷,这样生成关卡才快,你把编辑器交付给你的策划同事,他要面临的是生成几千个关卡。

  • 换成语和删成语,除了自由刷成语这个基本操作,应该支持对某个成语进行选中,把它换成其它更合适的成语,或者直接删掉重新编辑。

  • 去字功能,这也是编辑器比较重要的一个功能,因为在游戏中玩家看到的成语都不是完整的需要填空,而去字功能就是用来编辑那些字显示为空格需要让玩家填空。

  • 一个关卡少说有7,8个成语,如果一个一个的去点就太累了,这里我们实现了一个‘自动一键去字’功能,一键去字,如果效果不好,再手动微调即可。

  • 其它,关卡保存/关卡加载/成语词库配置读取

需求整理出来了,下一步就是简单设计和规划代码结构:
1. 词条基本数据
我们希望用一个类来描述成语词条的基本数据,请记住,它对应的仅仅
是成语词库里的一条数据,而不是成语对象。但是很明显,最终它会被一个成语对象所引用。
词条基本数据所需要的数据结构很简单:
//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;        //成语的出处和释义
    }
    //...
}

2. 成语对象
我们希望用一个类来描述关卡中编辑出的每一条成语对象
成语对象的成员也很简单:
  • 记录自己占用了哪些格子(因为后续的操作例如换词/删词都会使用这个数据)
  • 引用成语词条数据
  • 记录编辑时,自身的方向(就两个方向:横 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]);
            }
        }
        //...
    }	
    
    

3. 格子对象
我们希望用一个类来描述编辑区每个格子的状态与行为:
  • 该格子是否被使用了

  • 该格子上面的成语字符(如果没被使用,就是” ”)

  • 该格子对应的成语对象(格子上需要记录成语对象,并且要以数组形式记录,因为存在两个成语交叉字格子)

  • 该格子是否是被共享的格子

  • 该格子是否是被‘去字’的状态

重点就是以上属性,当然肯定有一些显示相关属性就不一一列出了
//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;
	
    },	
})
4. 关卡对象
我们希望用一个类来描述游戏关卡对象。 关卡对象组织了格子和成语对象,并且负责对刷成语和换成语/删成语/去字/保存加载等
因此关卡类会包含更多的数据与行为,事实上大部分代码也集中在关卡类中,关卡对象主要的数据成员为:
  • 一个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. 自动去字
640?wx_fmt=png
实现自动去字主要是为编辑时提供一个快捷功能,一个成语最多自动去掉两个字写了一个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();
    }
}

以上是关于刷词和去字的实现,至于说其它如选词/换词/删词/保存加载,其实实现都很简单,成语项目整体来说很容易实现,这些功能就不啰嗦了。
顺便作一下下个项目的预告吧,下个项目准备介绍一个竖版跑酷类游戏—《峭壁逃亡》

640?wx_fmt=png

该游戏参与了 头条小游戏平台的内测
相关新闻链接:
 http://dy.163.com/v2/article/detail/DT5A07F10546236I.html
这个项目将重点给大家介绍 跑酷类游戏无限关卡的生成方法,以及基于 dragonBone 的角色控制, 敬请期待。
在此感谢「肖尧」的分享,「晓衡」微店中的《成语小状元》即是大佬的作品。公众号欢迎各位伙伴们来分享你的技术和作品,愿我们在前进的道路上砥砺前行,共同成长!
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值