Cocos Creator 开发微信小游戏

1,582 阅读11分钟

背景

今年开始学习cocos开发小游戏,上线了微信小游戏《跃上云端》,记录一下吧。

目标

小游戏已经上线,欢迎大家搜索《跃上云端》,这是一款在现有“跳一跳”源码基础上二次开发的游戏。通过操作屏幕点击移动控制小球运动的休闲弹跳类游戏,在游戏过程中可以通过跳中板心或者板边分别获得 2 分和 1 分,以及吃金币等方式获得分数。小游戏上线流程可以参考资料:#小游戏注册&上线指南,然后个人游戏是不需要的接入中宣部实名认证系统的,跳过即可。其余的审核资料网上都有一些参考,大家自行搜一搜;游戏审核的资料可以借助AI工具。

一、编辑器的使用

cocos面板包含了几下一些主要界面:

  1. 层级管理器:可以创建一些节点,比如sprite图片节点,创建之后可以在场景编辑器直接看到效果。
  2. 场景编辑器:内容创作的核心区域,摆放内容,搭建场景,获取所见即所得的预览效果。可以拖放缩放旋转元素。
  3. 资源管理器:
  • 树状结构,就跟平时我们的前端项目目录差不多。
  • cocos有internal内部资源,可以看到是被一个个小锁锁定的,说明不可修改,如果要修改可以点击复制到Asserts里面去自己修改。
  • 需要注意有一个特殊目录:resources。点击这个文件夹可以看到右侧的说明,只能通过resouces.load()去加载里面的资源,并且永远配置为一个Bundle。Bundle的作用是打包一些零零碎碎的所有的资源,减少请求。我做这个游戏的时候,由于微信对于包的4M大小限制,采用了分包的方法。
  1. 动画编辑器:用来制作跟预览动画,保存动画数据。
  2. 控制台:可以打印输出日志或者告警错误信息,跟浏览器的控制台很像。
  3. 属性检查器:查看并编辑当前选择的节点以及组件属性的工作区域。这里的组件可以是内置的组件,或者你自己创建的脚本组件,序列化在面板内,甚至可以监测到数据的变化。
  4. 项目预览:有3种浏览器预览;编辑器预览;模拟器预览。细节点是如果你有很多个场景时候,调试你只需要看某个场景,可以切换从某个场景开始预览。

小技巧:

  • 不小心把这些界面拖出来独立显示或者乱了,想恢复的话可以选择CocosCreater,布局->默认布局即可。
  • 选中任意一个节点,按F可以聚焦到这个节点,且使其显示在场景中间。
  • 锚点的概念:在2D模式下,一张图片会有9个点,中间的那个点就是锚点,就好像你在墙上给它按了一个图钉📌,做旋转动画的时候中心点就是这个点。其他8个点是用来调整Content Size的,即大小。
  • 左上角4个图标image.png快捷键 w(平移),e(旋转),r(缩放),t(矩形变换)。

二、游戏核心逻辑

image.png

1、跳板初始化

在整个游戏过程中,使用的板一共就只有 5 个,c存放在_boardList里面,后续的跳板生成都是通过复用的方式,不断的去重新计算位置以及序号。同时生成的board的类型有:正常,弹簧,冲刺跳板,大跳板。

image.png

    initBoard() {
        for (let i = 0; i < Constants.BOARD_NUM; i++) {
            const node = instantiate(this.boardPrefab) as Node;
            node.name = this._boardInsIdx.toString();
            const board = node.getComponent('Board') as Board;
            this.changeColor(board)
            this._boardInsIdx++;
            this.node.addChild(node);
          
            this._boardList.push(board);
    
        }
        this.reset();
    },
    // 每次开始游戏板重置
    reset(){
        this._boardInsIdx = 0;
        Constants.game.initFirstBoard = false;
        let pos = Constants.BOARD_INIT_POS.clone();
        let board: Board;
        const type = Constants.BOARD_TYPE.NORMAL;
        for (let i = 0; i < Constants.BOARD_NUM; i++) {
            board = this._boardList[i];
            board.reset(type, pos, 1);
            pos = this.getNextPos(board, 1);
        }

        board = this._boardList[0];
        board.isActive = true;
        Constants.game.ball.currBoard = board;

        if (this.diamondSprintList[0]) {
            for (var i = 0; i < Constants.DIAMOND_NUM; i++) {
                this.diamondSprintList[i].active = false;
            }
        }
    }

2、屏幕事件监听,小球与普通板块弹跳计算。

初始化完跳板之后,要开始做小球的弹跳。整个游戏的入口函数都设定在 Game 类上,Game 又添加在 Canvas 节点上,因此,Game 类所挂载的节点就作为全局对象的事件监听节点来使用最合适不过。因为主要接受该事件的对象是小球,所以,我们在小球里做监听的回调。然后,小球根据一定比例的换算来做实际移动距离的计算。在 update 里每帧根据者冲刺等状态对小球进行调整。小球的上升与下降是通过模拟重力效果来实现。

    Constants.game.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
    Constants.game.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this);
    Constants.game.node.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
    onTouchStart(touch: Touch, event: EventTouch){
        this.isTouch = true;
        this.touchPosX = touch.getLocation().x;
        this.movePosX = this.touchPosX;
    },

    onTouchMove(touch: Touch, event: EventTouch){
        this.movePosX = touch.getLocation().x;
    }

    onTouchEnd(touch: Touch, event: EventTouch){
        this.isTouch = false;
    }

3. 提供相机跟随接口。

相机的移动位置不是由自身来操控的,而是根据小球当前的位置来进行实时跟踪。因此,相机只需要调整好设置接口,按照一定脱离距离去跟随小球即可。

    update() {
        _tempPos.set(this.node.position);
        if(_tempPos.x === this._originPos.x && _tempPos.y === this._originPos.y){
            return;
        }

        // 横向位置误差纠正
        if (Math.abs(_tempPos.x - this._originPos.x) <= Constants.CAMERA_MOVE_MINI_ERR) {
            _tempPos.x = this._originPos.x;
            this.setPosition(_tempPos);
        } else {
            const x = this._originPos.x - _tempPos.x;
            _tempPos.x += x / Constants.CAMERA_MOVE_X_FRAMES;
            this.setPosition(_tempPos);
        }

        _tempPos.set(this.node.position);
         // 纵向位置误差纠正
        if (Math.abs(_tempPos.y - this._originPos.y) <= Constants.CAMERA_MOVE_MINI_ERR) {
            _tempPos.y = this._originPos.y;
            this.setPosition(_tempPos);
        } else {
            const y = this._originPos.y - _tempPos.y;
            if (this.preType === Constants.BOARD_TYPE.SPRING) {
                _tempPos.y += y / Constants.CAMERA_MOVE_Y_FRAMES_SPRING;

                this.setPosition(_tempPos);
            } else {
                _tempPos.y += y / Constants.CAMERA_MOVE_Y_FRAMES;
                this.setPosition(_tempPos);
            }
        }
    }

同时相机更新的时候移动背景板:

    setPosition(position: Vec3) {
        this.node.setPosition(position);
        const y = position.y - 27;
        this.planeNode.setPosition(position.x, y, -100);
    }

同时游戏过程中,还对小球跟背景板进行了随机切换:

    switchBallbg(){
        if(Constants.game.score % 10 ===0){
            const child = this.node.getChildByName('RootNode')?.getChildByName('Sphere001')?.getComponent(MeshRenderer);
            const currentMaterial = child?.material?.getProperty('albedoMap');
            child?.material?.setProperty('albedoMap',currentMaterial===this.texture1?this.texture2:this.texture1 )
        }
        if(Constants.game.score % 20 ===0){
          // 查找到背景
         const bg =find('planeBg01/RootNode/Plane001')?.getComponent(MeshRenderer);
         console.log(bg,'bg')
         if(bg){
            const currentMaterial = bg?.material?.getProperty('albedoMap');
            console.log(currentMaterial,'currentMaterial',this.bgtexture1,'this.bgtexture1',this.bgtexture2)
            bg?.material?.setProperty('albedoMap',currentMaterial===this.bgtexture1?this.bgtexture2:this.bgtexture1 )
         }
        }
    }

我们定位元素的时候可以使用API - find docs.cocos.com/creator/3.5…

4.游戏开始与结束逻辑编写

游戏开始以及结束都是通过 UI 界面来实现。定义一个 UIManager 管理类来管理当前 UI 界面。所有的 UI 打开与关闭都通过此管理类来统一管理,点击事件的响应都直接回调给游戏主循环 Game 类。

实现如下(注意:cocos默认生成的代码,小程序没有分享给朋友或者朋友圈的功能,需要手动添加showShareMenu跟onShareAppMessage 函数,并且小程序里面wx是一个全局变量,无需引入):

    start() {
        if (typeof wx !== 'undefined') {
            wx.showShareMenu({
                withShareTicket: true,
                menus: ['shareAppMessage', 'shareTimeline']
            });
            wx.onShareAppMessage(() => {
                return {
                    title: '这游戏根本停不下来...',
                    imageUrl: 'https://xxxxx.com/game/img/logo_cloud.jpg' // 伪代码图片 URL
                }
            })
        }
        
        this.pageResult.active = false;
        this.gameLevelResult.active = false;
        this.currentLevel.string =`关卡 ${Constants.LEVEL.toString()}` ;
    }

在游戏结果函数:

    showGameLevelResult(visible:boolean){
        this.currentLevel.string =`关卡 ${Constants.LEVEL.toString()}` ;

        if(visible){
            Constants.LEVEL+=1;
        }
        this.pageLevel.string =`下一关:${Constants.LEVEL.toString()}` ;

        this.gameLevelResult.active = visible;
    }

5、小球运动逻辑

  1. 根据时间间隔和游戏常量更新游戏时间尺度(timeScale)

  2. 如果游戏状态为播放中(PLAYING),则执行以下操作(trailNode是拖尾效果):

    • 根据小球的跳跃状态进行不同的操作。
    • 如果小球处于冲刺状态(SPRINT),则判断小球是否已完成冲刺,若已完成则切换到跳跃状态(JUMPUP),重置相关变量,并清除小球路径上的钻石。
    • 更新小球的跳跃帧数,并根据小球位置与钻石位置判断是否收集到钻石,若收集到则增加分数、播放粒子效果并禁用该钻石。
    • 设置小球的坐标,并更新相机的高度。
    • 如果小球处于下落状态(FALLDOWN),则判断小球是否超过当前跳板的高度,如果超过则游戏结束;否则检测小球是否在当前跳板上,如果在则更新当前跳板和跳板索引,并激活该跳板。
    • 更新小球的跳跃帧数,设置小球的坐标。
    • 根据小球状态和跳跃帧数判断小球是否需要切换到下落状态,并重置相关变量。
    • 更新小球的轨迹位置。
  3. 如果游戏状态不是播放中,则不执行任何操作。

    update(deltaTime: number) {
        this.timeScale = Math.floor((deltaTime / Constants.normalDt) * 100) / 100;
        if (Constants.game.state === Constants.GAME_STATE.PLAYING) {
            const boardBox = Constants.game.boardManager;
            const boardList = boardBox.getBoardList();
            if (this.jumpState === Constants.BALL_JUMP_STATE.SPRINT) {
                // 冲刺状态结束后状态切换
                if (this.currJumpFrame > Constants.BALL_JUMP_FRAMES_SPRINT) {
                    this.jumpState = Constants.BALL_JUMP_STATE.JUMPUP;
                    this.isJumpSpring = false;
                    this.currJumpFrame = 0;
                    this.hasSprint = false;
                    // const eulerAngles = this.node.eulerAngles;
                    // this.node.eulerAngles = new Vec3(eulerAngles.x, -Constants.BALL_SPRINT_STEP_Y, eulerAngles.z);
                    boardBox.clearDiamond();
                }

                this.currJumpFrame += this.timeScale;
                const diamondSprintList = boardBox.getDiamondSprintList();
                for (let i = 0; i < Constants.DIAMOND_NUM; i++) {
                    if (Math.abs(this.node.position.y - diamondSprintList[i].position.y) <= Constants.DIAMOND_SPRINT_SCORE_AREA && Math.abs(this.node.position.x - diamondSprintList[i].position.x) <= Constants.DIAMOND_SPRINT_SCORE_AREA) {
                        Constants.game.addScore(Constants.DIAMOND_SCORE);
                        this.showScore(Constants.DIAMOND_SCORE);
                        Constants.game.ball.playDiamondParticle(this.node.position);
                        diamondSprintList[i].active = false;
                    }
                }
                this.setPosY();
                this.setPosX();
                // this.setRotY();
                this.touchPosX = this.movePosX;
                const y = this.node.position.y + Constants.CAMERA_OFFSET_Y_SPRINT;
                Constants.game.cameraCtrl.setOriginPosY(y);
            } else {
                for (let i = this.currBoardIdx + 1; i >= 0; i--) {
                    
                    const board = boardList[i];
                    if(!board){
                     continue;
                    }
                    const pos = this.node.position;
                    const boardPos = boardList[i].node.position;
                    if (Math.abs(pos.x - boardPos.x) <= boardList[i].getRadius() && Math.abs(pos.y - (boardPos.y + Constants.BOARD_HEIGTH)) <= Constants.DIAMOND_SCORE_AREA) {
                        boardList[i].checkDiamond(pos.x);
                    }

                    // 超过当前跳板应该弹跳高度,开始下降
                    if (this.jumpState === Constants.BALL_JUMP_STATE.FALLDOWN) {
                        if (this.currJumpFrame > Constants.PLAYER_MAX_DOWN_FRAMES || (this.currBoard.node.position.y - pos.y) - (Constants.BOARD_GAP + Constants.BOARD_HEIGTH) > 0.001) {
                            ParticleUtils.stop(this.trailNode!);
                            Constants.game.gameDie();
                            return;
                        }

                        // 是否在当前检测的板上
                        if (this.isOnBoard(board)) {
                            this.currBoard = board;
                            this.currBoardIdx = i;   
                
                            this.activeCurrBoard(boardList);
                            break;
                        }
                    }
                }

                this.currJumpFrame += this.timeScale;

                if (this.jumpState === Constants.BALL_JUMP_STATE.JUMPUP) {
                    if (this.isJumpSpring && this.currJumpFrame >= Constants.BALL_JUMP_FRAMES_SPRING) {
                        // 处于跳跃状态并且当前跳跃高度超过弹簧板跳跃高度
                        this.jumpState = Constants.BALL_JUMP_STATE.FALLDOWN;
                        this.currJumpFrame = 0;
                    } else {
                        if (!this.isJumpSpring && this.currJumpFrame >= Constants.BALL_JUMP_FRAMES) {
                            // 跳跃距离达到限制,开始下落
                            this.jumpState = Constants.BALL_JUMP_STATE.FALLDOWN;
                            this.currJumpFrame = 0;
                        }
                    }
                }

                this.setPosY();
                this.setPosX();
                // this.setRotZ();

                if (this.currBoard.type !== Constants.BOARD_TYPE.SPRINT) {
                    Constants.game.cameraCtrl.setOriginPosX(this.node.position.x);
                }

                this.touchPosX = this.movePosX;
            }

            this.setTrailPos();
        }
    }

6、其他3D相关知识

首先得了解几个概念: 模型:通常cocos里面使用的是fbx文件,可以让我们的设计师提供,或者网上找。我们可以导入资源(直接放到你的model文件夹即可)查看。 材质(Material)
材质定义了物体表面的光学特性,如颜色、反射率、透明度等。它决定了物体在光照下的表现。材质可以包含以下属性:

  • 颜色(Color) :物体的基本颜色。
  • 反射率(Reflectivity) :物体对光线的反射程度。
  • 透明度(Transparency) :物体的透明程度。
  • 粗糙度(Roughness) :物体表面的粗糙程度,影响反射光线的散射。
  • 金属度(Metallic) :表示物体是否具有金属特性。

通过调整材质的属性,可以创建出各种不同外观的物体,如金属、塑料、玻璃等。

纹理(Texture)
纹理是一张图像,用于覆盖物体的表面,增加细节和真实感。纹理可以是颜色纹理、法线纹理、粗糙度纹理等。常见的纹理类型包括:

  • 颜色纹理(Color Texture) :提供物体表面的颜色信息。
  • 法线纹理(Normal Texture) :用于模拟物体表面的凹凸细节,增强光照效果。
  • 粗糙度纹理(Roughness Texture) :控制物体表面的粗糙度。
  • 环境光遮蔽纹理(Ambient Occlusion Texture) :模拟物体之间的阴影效果。

纹理可以通过图像编辑软件创建,也可以从外部资源(如图片库)中获取。在 3D 场景中,纹理通常被映射到物体的表面上,以增加细节和真实感。

材质和纹理通常一起使用,材质定义了物体的基本特性,而纹理则提供了更详细的表面细节。通过合理地选择和应用材质和纹理,可以创建出逼真的 3D 物体和场景。

image.png 如上图,mesh可以理解为物体的本身,由一系列的顶点、边和面组成,用于定义物体的形状和几何结构;而meterial可以理解为物体的皮肤。

image.png 如上图,是我设置的一个材质,使用了贴图文件。 所以这里我们的操作步骤可以如下:

  • cocos里面模型使用的是fbx文件
  • 新建预制体A,设置mesh为第一步的模型
  • 然后新建材质M,设置贴图
  • 再给A的材质设置为M

这里预制件用于存储一些可以复用的场景对象,它可以包含节点、组件以及组件上的数据。由预制件生成的实例既可以继承模板的数据,又可以有自己定制化的数据修改。创建预制件有两种方法:

  1. 在场景中将节点编辑好之后,直接将节点从 层级管理器 拖到 资源管理器 中即可完成预制件资源的创建。
  2. 点击 资源管理器 左上方的  +  按钮,或者点击面板空白处,然后选择 Node Prefab 即可。 双击预制体,进入预制件编辑模式,场景编辑器左上角有两个按钮“保存”与“关闭”,这两个按钮非常不显眼,我之前就没有看清楚导致我每次关闭还要去双击Scene😓。

三、调试方法

目前有一下调试方法:

  1. 观察法:一般用来调试不太能定量的问题,具体我还没有运用到。
  2. 编辑器调试:直接在编辑器预览,比较可视化,去调整修改节点的属性值,比如position,这个时候数据是没有被保存的,但是右上角有个...点击“复制节点的值”,再停止预览,再次选择节点右上角的...选择“粘贴节点的值”,这样调试的结果就可以看到了。 image.png
  3. 日志调试法 比如在start方法里面
    console.log(this.node.position, this.node.rotation); // 输出一个三位向量Vec3

可以在编辑器或者浏览器的控制台里面。 4.断点调试就是浏览器直接打断点。

四、二维码

大家可以扫码体验一下,多多支持哦~ image.png