欢迎访问 生活随笔!

生活随笔

当前位置: 首页 > 编程资源 > 编程问答 >内容正文

编程问答

成语json_cocos creator实战(2)成语小秀才ts版

发布时间:2025/4/5 编程问答 49 豆豆
生活随笔 收集整理的这篇文章主要介绍了 成语json_cocos creator实战(2)成语小秀才ts版 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

1 分析

公司要求做h5小游戏之前,想要做的一款类似成语小秀才的小游戏。学了一段时间ccc后回头填坑,尝试仿制一波,刚好发现论坛有套开源的ui素材。花了两天做的demo。做完后发现最难的是生成随机关卡,由于益智类小游戏动辄几百上千关,编写较高质量的关卡随机算法还是很有难度的。

  • 玩法:每个成语最多空缺两个位置,选择对应的词填入成语中,全对即可过关。
成语小秀才

界面分三部分,顶部关卡信息、成语区、选词区。

  • 成语区实际上是9*9的布局,共81的格子,只显示了有词的格子,开启调试模式可以显示所有的格子。
  • 选词区显示成语中空缺的词
调试模式

2 代码结构

2.1 关卡数据结构

正式上线的话,需要开发服务端用于返回关卡数据和保存用户信息。客户端通过http请求获取关卡数据,同时可以上传分数等信息。demo中没有用到服务端,直接本地挂载关卡数据,关卡数据保存在json文件中。

id    ---> 成语id,对应成语库中的成语
grids ---> 保存成语中四个字的位置
           id ---> 所处的格子位置
           space ---> 该字是否为空缺
{
    "id": 4959,
    "grids": [
        {
            "id": 18,
            "space": false
        },
        {
            "id": 27,
            "space": true
        },
        {
            "id": 36,
            "space": false
        },
        {
            "id": 45,
            "space": true
        }
    ]
}

2.2 词条数据对象

用一个类来描述成语词条的基本数据,它对应词库中的一条成语。

// IdiomData.ts
export class IdiomData {
    public id: number = 0;          // 词条id
    public chars: string = null;    // 完整成语,例如"一马当先"
    public pinyin: string = null;   // 词条拼音
    public note: string = null;     // 词条出处和释义
}

2.3 词条对象

用一个类来描述关卡中出现的每一条成语对象。

  • 记录占用的格子
  • 保存词条数据对象
// Idiom.ts
export class Idiom {
    public grids = [];      // 记录占用的格子
    public data = null;     // 词条数据
    public constructor(grids, idiomdataObj) {
        this.data = idiomdataObj;
        for (let i = 0; i             this.grids.push(grids[i]);
        }
    }
}

2.4 格子对象

用一个类来描述每个格子的状态和行为。

  • 记录格子id
  • 该格子使用状态
  • 保存完整的成语数据
  • 格子上需要填写的字符
  • 保存被哪些词条对象使用
  • 格子是否为空缺状态
  • 格子填词是否成功,用来判断填词是否成功
  • 当前格子上的词id
// Grid.ts
    public gridId: number = 0;                  // 格子id
    public isUsed: boolean = false;             // 是否是被使用的格子
    public data: string = null;                 // 完整的成语
    public char: string = "";                   // 使用的成语字符,单个字
    public idioms = [];                         // 反向保存idiom引用    这个是用来保存这个格子被哪些词条引用
    public isSpaceGrid: boolean = false;        // 是否是 被 去字 状态
    public isSelectMode: boolean = false;       // 是否是 填字 模式
    public isSuccess: boolean = false;          // 标注该位置词 是否正确
    public currentId: number = 0;               // 记录当前格子上 选词 的id,用来判断回退
    
    // 重置 格子数据,关卡切换复用
    public resetGrid() {
        ……
    }
    // 格子注册监听事件
    public addListener(fn: Function, target) {
        this.node.on(cc.Node.EventType.TOUCH_END, fn, target);
    }
    // 格子移除监听事件
    public removeListener(fn: Function, target) {
        this.node.off(cc.Node.EventType.TOUCH_END, fn, target);
    }

2.5 关卡对象

用一个类来描述游戏关卡。关卡对象读取关卡json文件,返回词条对象数组。http请求获取关卡数据可以放在这里。

// Level.ts
    public initLevelData() {
        let idioms = this._levelData.json["idioms"];
        let idiomArr = [];
        for (let i = 0; i             let _id = idioms[i].id
            let idiomdataObj = new IdiomData(this._jsonData.json[_id]);
            // 词条对象 存词条数据,占位
            let idiomObj: Idiom = new Idiom(idioms[i].grids, idiomdataObj);
            idiomArr.push(idiomObj);
        }
        return idiomArr;
    }

3 界面设计

3.1 节点结构

节点结构

3.2 结算界面

展示词条成语,提供下一关切换按钮。

节点结构

3.3 词条详情界面

展示词条拼音、释义、出处。

词条详情

游戏逻辑

4.1 成语区

  • 制作一个格子的预制体,一次性创建81个格子对象。
格子预制体// game.ts
    private init() {
        // 9*9=81块    rows col
        for (let index = 0; index 81; index++) {
            let node = cc.instantiate(this.piece_prefab);
            node.parent = this.qipan;
            let tile: Grid = node.getComponent(Grid);
            tile.gridId = index;
            if (!this.isDebug) {
                tile.hide();
            }
            this.tiles.push(tile);
        }
        …… ……
    }
  • 遍历成语对象后,给指定格子位置添加对应的成语字符并绑定数据。从成语对象中拿到成语,将每个成语都分割成单独的字符存入数组中,将字符循环插入到格子中,如果该位置的格子为去字状态,就把该位置的字符隐藏,并记录空缺字符的id和字符用于初始化选词区。
        // 遍历成语对象 
        for (let i = 0; i             let chars = idiomArr[i].data.chars;
            let grids = idiomArr[i].grids;
            // 分割拿到单独的字符
            let arr = chars.split("");
            for (let j = 0; j                 let gid = grids[j].id;
                let space = grids[j].space;
                this.tiles[gid].bg.node.active = true;
                this.tiles[gid].word.node.active = true;
                // 设置 格子信息
                this.tiles[gid].gridId = gid;
                this.tiles[gid].char = arr[j];
                this.tiles[gid].data = chars;
                this.tiles[gid].idioms.push(idiomArr[i]);
                // 判断是否为 去字 状态,如果是去字,那么就隐藏上面的word
                if (space) {
                    this.tiles[gid].isSpaceGrid = true;     // 空格子
                    if (!this.tiles[gid].isSelectMode) {    // 判断 该空是否为选词
                        this.tiles[gid].isSelectMode = true;
                        this.tiles[gid].bg.spriteFrame = this.word_tile;
                        // 添加点击事件
                        this.tiles[gid].addListener(this.onSpaceGridClick, this);
                        this.selectWords.push({"id": gid, "char": arr[j]});
                    }
                } else {
                    this.tiles[gid].word.string = arr[j];
                    this.tiles[gid].bg.spriteFrame = this.word_normal;
                }
            }
        }
  • 空位置触摸事件。空位置需要实现两个功能:显示字符和回退选词区。
        // 标注空位置上已经有词  回退到选择区
        if (grid.isUsed) {
            let char = grid.word.string;
            this.tiles[gridId].isUsed = false;
            this.tiles[gridId].word.string = "";
            this.tiles[gridId].isSuccess = false;
            this.tiles[gridId].bg.spriteFrame = this.word_selected;
            this.currentSelectGrid = gridId;
            let currentid = grid.currentId;
            for (let i = 0; i this.selectNode.length; i++) {
                if (this.selectNode[i].gridId === currentid) {
                    this.selectNode[i].node.getComponent(cc.Animation).play("show_word");
                }
            }
        }

4.2 选词区

  • 复用格子预制体,遍历记录空缺字符的selectWordsArr数组里的数据。
选词区    // 初始化选词区
    private initSelectGroup() {
        for (let s = 0; s this.selectWords.length; s++) {
            let gid = this.selectWords[s].id;
            let char = this.selectWords[s].char;

            let node = cc.instantiate(this.piece_prefab);
            node.parent = this.selectGroup;
            let tile: Grid = node.getComponent(Grid);
            tile.gridId = gid;
            tile.char = char;
            tile.word.string = char;
            tile.addListener(this.onSelectGridClick, this);

            this.selectNode.push(tile);
        }
    }
  • 为了可玩性,随机打乱selectWordsArr里的数据,将选词打乱。
    // 随机打乱selectWords数组
    this.selectWords.sort(() => {
        return Math.random() > 0.5 ? -1 : 1;
    });
  • 选词区触摸回调,判断选词与空缺位置是否匹配,如果匹配将该格子的isSuccess设为true,表明该位置字符正确。这里判断字符正确的依据不能通过比较id的方式,因为有可能选词区出现若干个相同字符的格子,所以选择任意一个都要能达到字符正确的效果。
    // 选词 回调事件 ---> 判断与当前 选中位置 是否相符
    private onSelectGridClick(e) {
        …… ……
        // 隐藏该选词 
        e.target.getComponent(cc.Animation).play("hide_word");
        if (this.tiles[this.currentSelectGrid].isUsed) {
            let char = this.tiles[this.currentSelectGrid].word.string;
            let nowId = this.tiles[this.currentSelectGrid].currentId;
            this.selectNode[nowId].node.active = true;
            this.tiles[this.currentSelectGrid].word.string = grid.char;
        } else {
            this.tiles[this.currentSelectGrid].word.string = grid.char;
            this.tiles[this.currentSelectGrid].isUsed = true;
            this.tiles[this.currentSelectGrid].bg.spriteFrame = this.word_finished;
            this.tiles[this.currentSelectGrid].currentId = gridId;
        }
        // 判断是否填词成功  通过比较字符的方式 gridId === this.currentSelectGrid
        if (char === this.tiles[this.currentSelectGrid].char) {
            // 记录该位置词语正确
            this.tiles[this.currentSelectGrid].isSuccess = true;
        }
        this.judgeSuccess(this.currentSelectGrid);
    }

4.3 填词逻辑

  • 每填一个词都要判断格子上绑定的成语是否填词成功。找出空位置,然后判断空位置上所有的isSuccess是否为true,满足条件则播放填词动画,查找下一个空缺位置。
    // 判断词条成功
    private judgeSuccess(gridId: number) {
        …… ……
        if (this.tiles[gridId].idioms.length > 0) {
            for (let i = 0; i this.tiles[gridId].idioms.length; i++) {
                let idiom = this.tiles[gridId].idioms[i];
                let flag = true;
                spaceArr = [];
                // 遍历去字 位置
                for (let j in idiom.grids) {
                    let id = idiom.grids[j].id;
                    if (idiom.grids[j].space === true) {
                        spaceArr.push(id);
                    }
                }
                // 判断成功
                for (let s = 0; s                     if (this.tiles[spaceArr[s]].isSuccess !== true) {
                        flag = false;
                    }
                }
            // 填词动画后,找下一个 空位置
            this.findNextSpaceGrid();
            …… ……
    }

4.4 过关逻辑

  • 寻找下一个空位置。遍历格子对象,筛选isSelectMode为true同时isUsed为false,也就是未被占用的格子。满足条件的格子设置当前选中状态即可。
寻找空位置        for (let i = 0; i this.tiles.length; i++) {
            if (this.tiles[i].isSelectMode && this.tiles[i].isUsed === false) {
                // 记录选中的格子位置
                this.currentSelectGrid = i;
                this.tiles[i].bg.spriteFrame = this.word_selected;
                return;
            }
        }
  • 如果寻找下一个空位置失败,那么说明不存在下一个空格子,就要判断过关。
        // 如果没有下一个格子 就判断是否过关
        let flag = true;
        for (let sw = 0; sw this.selectWords.length; sw++) {
            let id = this.selectWords[sw].id;
            if (this.tiles[id].isSuccess !== true) {
                flag = false;
            }
        }
        if (flag) {
            // 执行过关函数
            cc.log("过关");
            this.gameSuccess();
        }

4.5 动画效果

  • 选词触摸动画,对照小秀才发现当选词点击时只需要把scale执行从1到0的动画就可以达到同样的效果。如果直接将active置false,layout的格子布局就会自动调整可见选词区的位置,导致每个选词的位置出现变化。
选词触摸动画
  • 填词动画。对比发现,填词正确动画拆分看,每个位置的动画都是一样的,将格子进行快速缩放并设置不同的延迟,看上去有跳动的效果。
填词动画    // 填词成功动画
    private fillGrid(grids, arr) {
        for (let i = 0; i             let id = grids[i].id;
            this.tiles[id].isSelectMode = false;
            // 每个字延迟动画
            setTimeout(() => {
                this.tiles[id].getComponent(cc.Animation).play("word_success");
            }, 100 * i);
        }
        for (let j = 0; j             this.tiles[arr[j]].removeListener(null, this);
        }
    }

4.6 词条成功逻辑

  • 判断词条中空位置格子是否都正确,如果正确就执行填词动画,查找下一个空位。如果没有空位置且填词全都正确,则说明过关,展示结算界面。
结算界面
  • 词条详情界面,显示拼音、释义、出处。
词条详情

5 演示

demo完整演示

6 总结

  • 项目开始前把结构划分清除,每个模块之间的联系都确定好,可以提高开发效率。
  • 第一次仿制游戏,得益于有完整的ui素材,论坛也有类似的帖子介绍。拿这类的小游戏练手对提高学习信心很有帮助。
  • 关于随机关卡,跟坛友交流过,随机生成的成语质量无法保证,也就是有可能生僻词混搭,能实现但体验会变差。还有另一种手动标注成语,人工干预的方式去生成关卡。随机关卡思路是有了,但是具体没实现,暂时用不上。什么时候有心情做了再发出来。
  • 使用cocos creator2.3.2版本,typescript语言

总结

以上是生活随笔为你收集整理的成语json_cocos creator实战(2)成语小秀才ts版的全部内容,希望文章能够帮你解决所遇到的问题。

如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。