背景
今年开始学习cocos开发小游戏,上线了微信小游戏《跃上云端》,记录一下吧。
目标
小游戏已经上线,欢迎大家搜索《跃上云端》,这是一款在现有“跳一跳”源码基础上二次开发的游戏。通过操作屏幕点击移动控制小球运动的休闲弹跳类游戏,在游戏过程中可以通过跳中板心或者板边分别获得 2 分和 1 分,以及吃金币等方式获得分数。小游戏上线流程可以参考资料:#小游戏注册&上线指南,然后个人游戏是不需要的接入中宣部实名认证系统的,跳过即可。其余的审核资料网上都有一些参考,大家自行搜一搜;游戏审核的资料可以借助AI工具。
一、编辑器的使用
cocos面板包含了几下一些主要界面:
- 层级管理器:可以创建一些节点,比如sprite图片节点,创建之后可以在场景编辑器直接看到效果。
- 场景编辑器:内容创作的核心区域,摆放内容,搭建场景,获取所见即所得的预览效果。可以拖放缩放旋转元素。
- 资源管理器:
- 树状结构,就跟平时我们的前端项目目录差不多。
- cocos有internal内部资源,可以看到是被一个个小锁锁定的,说明不可修改,如果要修改可以点击复制到Asserts里面去自己修改。
- 需要注意有一个特殊目录:resources。点击这个文件夹可以看到右侧的说明,只能通过resouces.load()去加载里面的资源,并且永远配置为一个Bundle。Bundle的作用是打包一些零零碎碎的所有的资源,减少请求。我做这个游戏的时候,由于微信对于包的4M大小限制,采用了分包的方法。
- 动画编辑器:用来制作跟预览动画,保存动画数据。
- 控制台:可以打印输出日志或者告警错误信息,跟浏览器的控制台很像。
- 属性检查器:查看并编辑当前选择的节点以及组件属性的工作区域。这里的组件可以是内置的组件,或者你自己创建的脚本组件,序列化在面板内,甚至可以监测到数据的变化。
- 项目预览:有3种浏览器预览;编辑器预览;模拟器预览。细节点是如果你有很多个场景时候,调试你只需要看某个场景,可以切换从某个场景开始预览。
小技巧:
- 不小心把这些界面拖出来独立显示或者乱了,想恢复的话可以选择CocosCreater,布局->默认布局即可。
- 选中任意一个节点,按F可以聚焦到这个节点,且使其显示在场景中间。
- 锚点的概念:在2D模式下,一张图片会有9个点,中间的那个点就是锚点,就好像你在墙上给它按了一个图钉📌,做旋转动画的时候中心点就是这个点。其他8个点是用来调整Content Size的,即大小。
- 左上角4个图标
快捷键 w(平移),e(旋转),r(缩放),t(矩形变换)。
二、游戏核心逻辑
1、跳板初始化
在整个游戏过程中,使用的板一共就只有 5 个,c存放在_boardList里面,后续的跳板生成都是通过复用的方式,不断的去重新计算位置以及序号。同时生成的board的类型有:正常,弹簧,冲刺跳板,大跳板。
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、小球运动逻辑
-
根据时间间隔和游戏常量更新游戏时间尺度(timeScale)
-
如果游戏状态为播放中(PLAYING),则执行以下操作(trailNode是拖尾效果):
- 根据小球的跳跃状态进行不同的操作。
- 如果小球处于冲刺状态(SPRINT),则判断小球是否已完成冲刺,若已完成则切换到跳跃状态(JUMPUP),重置相关变量,并清除小球路径上的钻石。
- 更新小球的跳跃帧数,并根据小球位置与钻石位置判断是否收集到钻石,若收集到则增加分数、播放粒子效果并禁用该钻石。
- 设置小球的坐标,并更新相机的高度。
- 如果小球处于下落状态(FALLDOWN),则判断小球是否超过当前跳板的高度,如果超过则游戏结束;否则检测小球是否在当前跳板上,如果在则更新当前跳板和跳板索引,并激活该跳板。
- 更新小球的跳跃帧数,设置小球的坐标。
- 根据小球状态和跳跃帧数判断小球是否需要切换到下落状态,并重置相关变量。
- 更新小球的轨迹位置。
-
如果游戏状态不是播放中,则不执行任何操作。
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 物体和场景。
如上图,mesh可以理解为物体的本身,由一系列的顶点、边和面组成,用于定义物体的形状和几何结构;而meterial可以理解为物体的皮肤。
如上图,是我设置的一个材质,使用了贴图文件。
所以这里我们的操作步骤可以如下:
- cocos里面模型使用的是fbx文件
- 新建预制体A,设置mesh为第一步的模型
- 然后新建材质M,设置贴图
- 再给A的材质设置为M
这里预制件用于存储一些可以复用的场景对象,它可以包含节点、组件以及组件上的数据。由预制件生成的实例既可以继承模板的数据,又可以有自己定制化的数据修改。创建预制件有两种方法:
- 在场景中将节点编辑好之后,直接将节点从 层级管理器 拖到 资源管理器 中即可完成预制件资源的创建。
- 点击 资源管理器 左上方的 + 按钮,或者点击面板空白处,然后选择 Node Prefab 即可。 双击预制体,进入预制件编辑模式,场景编辑器左上角有两个按钮“保存”与“关闭”,这两个按钮非常不显眼,我之前就没有看清楚导致我每次关闭还要去双击Scene😓。
三、调试方法
目前有一下调试方法:
- 观察法:一般用来调试不太能定量的问题,具体我还没有运用到。
- 编辑器调试:直接在编辑器预览,比较可视化,去调整修改节点的属性值,比如position,这个时候数据是没有被保存的,但是右上角有个...点击“复制节点的值”,再停止预览,再次选择节点右上角的...选择“粘贴节点的值”,这样调试的结果就可以看到了。
- 日志调试法 比如在start方法里面
console.log(this.node.position, this.node.rotation); // 输出一个三位向量Vec3
可以在编辑器或者浏览器的控制台里面。 4.断点调试就是浏览器直接打断点。
四、二维码
大家可以扫码体验一下,多多支持哦~