此文章是本人第一次学习CocosCreator所记录的心得体会,写下此文章的目的有两个:
- 增强学习过程的记忆
- 记录一下关键点,供后续查阅,也想和刚学习的小伙伴一起研究探讨
最终效果图:
环境配置
- 游戏开发工具:Cocos Creator 3.8.3
- 脚本代码编辑工具:VSCode
- 练习用的游戏资源下载:链接:百度网盘游戏资源下载 提取码:l2sa
- 完整代码见文章底部
学习过程
一、前期准备
1、使用CocosCreator创建一个Empty2D项目
2、在assets文件夹下创建文件夹
- scene:场景存放
- animation:动画存放
- images:图片资源存放
- prefab:预制体存放
- scripts:脚本代码存放
文件夹创建完成后按下Ctrl+s进行保存,保存在assets/scene文件夹下,文件取名为game(可根据实际随意)
3、导入图片资源
将上述网盘中的图片资源导入,在assets/images上右键导入新文件,选中后缀为png的图片导入即可
结果图:
二、小鸟
1、在Canvas下创建Sprite节点,重命名为Bird,用作小鸟节点
(刚开始学习有点疑惑,什么时候用空节点,什么时候用Sprite,后来在文档中看到了组件的概念,发现两者的区别仅仅是:Sprite节点在空节点上自动添加了Sprite组件而已)。
将assets/images下的bird1图片拖动到Bird节点的SpriteFrame上,就会展示小鸟的样子了。
2、接下来让小鸟动起来
在assets/animation文件夹下创建动画剪辑(Animation Clip),重命名为birdAnimation,将birdAnimation拖到Bird节点的属性检查器面板,会自动添加上Animation组件
3、动画编辑
选中Bird,进入动画编辑器
在属性列表添加SpriteFrame,选中assets/images下的bird1、bird2、bird3三张图片,一起拖动到动画编辑器的关键帧上。
分别调整三个小鸟到0、10、20位置(数字表示时间间隔),在第30的位置复制第一张bird1图片(小鸟飞是一个循环动画的过程,形成闭环后动画更加自然)
最后勾上加载后播放,循环模式选择循环播放,保存动画,保存场景后预览,小鸟就会一直飞
三、背景图
1、设计尺寸
在项目设置中修改设计尺寸,设计宽度288,设计高度512(也可自行设定其他尺寸),目前对于设计尺寸的认知是游戏制作时设定一个分辨率用于各种资源的布局,后续发布时会以设计尺寸去适配各种不同的屏幕尺寸。
2、背景图创建
在Canvas下创建一个Sprite节点,重命名为Bg,拖动assets/images下的background图片至Bg节点的SpriteFrame,
并调整ContentSize为288,512,Position为0,0(此时小鸟看不见了,因为渲染的顺序时自上而下,所以拖动调整Bg节点至Bird节点上方,先渲染背景图,再渲染小鸟)
3、小鸟前进?背景图后退
在assets/scripts下创建一个MainController.ts脚本,并将MainController添加到Canvas上,思路:背景图创建两个一摸一样的节点,区别是一个x坐标为0,另一个为288。
将两个背景节点赋值给脚本,在脚本的update中每次x坐标都减少1个像素,形成向左移动的效果,当左边背景图移出画面(x坐标<=-288)时马上从左边移动到最右边(x坐标变成288),以此往复循环
import { _decorator, BoxCollider2D, Button, Collider2D, Component, Director, director, EventTouch, instantiate, Label, log, Node, Prefab, Quat, Sprite, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('MainController')
export class MainController extends Component {
@property(Label)
label: Label = null;
@property(Sprite)
SpBg: Sprite[] = [null, null];
start() {
}
update(deltaTime: number) {
for (var i = 0; i < 2; i++) {
var pos = this.SpBg[i].node.getPosition();
pos.x -= 1.0;
if (pos.x <= -288) {
pos.x = 288;
}
this.SpBg[i].node.setPosition(pos)
}
}
}
四、水管障碍物
1、水管图创建
在Canvas下创建一个空节点,命名为Pipe,用来存放上水管和下水管。在Pipe节点下创建两个Sprite节点,分别命名为PipeUp和PipeDown。将assets/images下的pipe图片与之前一样,拖动到两个Sprite节点的SpriteFrame上。调整ContentSize属性至水管大小合适,调整position至位置合适即可。
2、制作水管预制体
游戏运行时,水管会以不同的高度持续出现,预制体有点像代码里的Class类(目前的理解),后续可以在脚本中对预制体进行实例化,从而创建多个水管。直接将Canvas下的Pipe节点拖动到assets/prefab下即可制作预制体,制作成功后删除Canvas下的Pipe节点。
这里为了控制Pipe的层级,在Canvas下新建一个空节点,命名为Pipe,并移动至Bird节点上层
3、生成水管
在MainController.ts脚本的start方法中初始化游戏开始时的水管。
start() {
for (var i = 0; i < 3; i++) {
this.pipe[i] = instantiate(this.pipePrefab) // 实例化水管预制体
this.node.getChildByName("Pipe").addChild(this.pipe[i]) // 找到Pipe节点,在Pipe节点下生成水管,便于控制层级
var pos = this.pipe[i].getPosition()
pos.x = 170 + 200*i // x坐标为水管间隔距离
var minY = -170
var maxY = 170
pos.y = minY + Math.random()*(maxY-minY) // 水管上下高度随机在范围内变化
this.pipe[i].setPosition(pos)
}
}
在update方法中处理水管向后移动(与背景图处理类似),并随机生成后续水管。
update(deltaTime: number) {
// 背景图向后移动处理
for (var i = 0; i < 2; i++) {
var pos = this.SpBg[i].node.getPosition();
pos.x -= 1.0;
if (pos.x <= -288) {
pos.x = 288;
}
this.SpBg[i].node.setPosition(pos)
}
// 水管向后移动并生成新水管
for(var i=0;i<3;i++){
var pos = this.pipe[i].getPosition()
pos.x -= 1.0 // 向后移动处理
if(pos.x <= -170){ // 移出画面后变为新的水管
pos.x = 430
var minY = -170
var maxY = 170
pos.y = minY + Math.random()*(maxY - minY)
}
this.pipe[i].setPosition(pos)
}
}
MainController完整代码:
import { _decorator, BoxCollider2D, Button, Collider2D, Component, Director, director, EventTouch, instantiate, Label, log, Node, Prefab, Quat, Sprite, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('MainController')
export class MainController extends Component {
@property(Sprite)
SpBg: Sprite[] = [null, null];
@property(Prefab)
pipePrefab: Prefab = null;
pipe: Node[] = [null, null, null];
start() {
for (var i = 0; i < 3; i++) {
this.pipe[i] = instantiate(this.pipePrefab) // 实例化水管预制体
this.node.getChildByName("Pipe").addChild(this.pipe[i]) // 找到Pipe节点,在Pipe节点下生成水管,便于控制层级
var pos = this.pipe[i].getPosition()
pos.x = 170 + 200*i // x坐标为水管间隔距离
var minY = -170
var maxY = 170
pos.y = minY + Math.random()*(maxY-minY) // 水管上下高度随机在范围内变化
this.pipe[i].setPosition(pos)
}
}
update(deltaTime: number) {
// 背景图向后移动处理
for (var i = 0; i < 2; i++) {
var pos = this.SpBg[i].node.getPosition();
pos.x -= 1.0;
if (pos.x <= -288) {
pos.x = 288;
}
this.SpBg[i].node.setPosition(pos)
}
// 水管向后移动并生成新水管
for(var i=0;i<3;i++){
var pos = this.pipe[i].getPosition()
pos.x -= 1.0 // 向后移动处理
if(pos.x <= -170){ // 移出画面后变为新的水管
pos.x = 430
var minY = -170
var maxY = 170
pos.y = minY + Math.random()*(maxY - minY)
}
this.pipe[i].setPosition(pos)
}
}
}
将assets/prefab下的pipe预制体拖至脚本上即可预览效果。
五、小鸟操作
1、小鸟上升与下坠
在不点击的情况下,小鸟一直下坠,点击屏幕时,小鸟向上飞翔一段时间。在assets/scripts下新疆BirdController.ts脚本,增加如下代码(用来控制小鸟的上升与下坠):
import { _decorator, BoxCollider2D, Collider2D, IPhysics2DContact, Canvas, Collider, Component, EventTouch, log, Node, Quat, Contact2DType } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('BirdController')
export class BirdController extends Component {
@property(Canvas)
Canvas: Canvas = null;
speed: number = 0;
protected onLoad(): void {
// 监听屏幕点击事件
this.Canvas.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
}
start() {
}
update(deltaTime: number) {
this.speed -= 0.05; // 每次速度减少
var pos = this.node.getPosition();
pos.y += this.speed; // 效果是每次加速一点点往下冲
this.node.setPosition(pos);
// 小鸟头朝向的角度处理
var angle = (this.speed / 2) * 30
if (angle >= 30) {
angle = 30;
}
this.node.setRotation(Quat.fromEuler(new Quat(), 0, 0, angle));
}
/**
* 点击屏幕时触发
*/
onTouchStart(event: EventTouch) {
this.speed = 2;
}
}
将BirdController.ts脚本拖动到Bird节点上,并将Canvas节点作为参数拖动到Bird上。
六、小鸟碰撞水管
1、碰撞回调
首次做碰撞回调,查阅了官方文档:Cocos3.8官方文档,Box2D 物理模块需要先在**RigidBody2D中 **开启碰撞监听,才会有相应的回调产生。开启方法为,在 Rigidbody2D 的 属性检查器 勾选 EnabledContactListener 属性,这里需要监听碰撞回调,是为了后续处理小鸟装上水管的游戏结束处理。
在Bird节点添加组件RigidBody2D和BoxCollider2D。勾选RigidBody2D的EnabledContactListener属性,开启碰撞回调监听,设置RigidBody2D的GravityScale属性值为0,关闭重力值。
编辑assets/prefab下的Pipe预制体,给PipeUp和PipeDown两个节点添加组件RigidBody2D和BoxCollider2D。分别设置RigidBody2D的GravityScale属性值为0,关闭重力值。(!记得预制体修改完一定要点击保存)
2、碰撞代码处理
小鸟碰撞水管后即为游戏结束,这里涉及了游戏状态,在MainController.ts中增加游戏状态枚举:
export enum GameStatus{
Game_Ready = 0, // 游戏准备
Game_Playing, // 游戏进行中
Game_Over, // 游戏结束
}
在MainController.ts中增加一个游戏结束的处理函数:
gameOver(){
this.gameStatus = GameStatus.Game_Over
log('游戏结束')
}
在BirdController.ts中监听碰撞回调,并在碰撞回调中调用MainController.ts中的gameOver()函数处理游戏结束:
import { _decorator, BoxCollider2D, Collider2D, IPhysics2DContact, Canvas, Collider, Component, EventTouch, log, Node, Quat, Contact2DType } from 'cc';
const { ccclass, property } = _decorator;
import { MainController,GameStatus } from './MainController';
@ccclass('BirdController')
export class BirdController extends Component {
@property(Canvas)
Canvas: Canvas = null;
speed: number = 0;
mainController: MainController = null;
protected onLoad(): void {
// 监听屏幕点击事件
this.Canvas.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
this.mainController = this.Canvas.getComponent(MainController)
}
start() {
let collider = this.getComponent(BoxCollider2D)
if (collider) {
collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
}
}
update(deltaTime: number) {
this.speed -= 0.05; // 每次速度减少
var pos = this.node.getPosition();
pos.y += this.speed; // 效果是每次加速一点点往下冲
this.node.setPosition(pos);
// 小鸟头朝向的角度处理
var angle = (this.speed / 2) * 30
if (angle >= 30) {
angle = 30;
}
this.node.setRotation(Quat.fromEuler(new Quat(), 0, 0, angle));
}
/**
* 点击屏幕时触发
*/
onTouchStart(event: EventTouch) {
this.speed = 2;
}
/**
* 只在两个碰撞体开始接触时被调用一次
* @param selfCollider 指的是回调脚本的节点上的碰撞体
* @param otherCollider 指的是发生碰撞的另一个碰撞体
* @param contact 碰撞主要的信息, 位置和法向量, 带有刚体的本地坐标来,
*/
onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
this.mainController.gameOver() // 调用游戏结束函数
this.speed = 0
}
}
保存后预览,F12打开控制台,小鸟碰撞水管后控制台打印出“游戏结束”。
七、开始和结束界面
1、开始界面设计
在Canvas下创建一个Spirte节点,命名为GameStart,将assets/images下的logo图片拖动到GameStart节点的SpirteFrame属性上,游戏开始就以点击这个logo吧(这里也可以自行放置其他图片或者开始文字按钮之类),适当调整logo位置和大小
2、结束界面设计
在Canvas下创建一个Spirte节点,命名为GameOver,将assets/images下的restart图片拖动到GameOver节点的SpirteFrame属性上,游戏结束时展示这个Restart图片,点击后重新开始游戏,适当调整logo位置和大小
PS:这里为了更好的操作GameOver节点,可以将GameStart节点禁用(不渲染),点击GameStart节点属性左上角的框框
完成后将GameStart恢复渲染,将GameOver节点禁用。
3、脚本处理
在MainController.ts脚本中开启GameStart和GameOver节点的点击监听事件:
spGameOver: Sprite = null;
spGameStart: Sprite = null;
protected onLoad(): void {
// 开启点击监听
this.spGameOver = this.node.getChildByName("GameOver").getComponent(Sprite)
this.spGameOver.node.active = false
this.spGameStart = this.node.getChildByName("GameStart").getComponent(Sprite)
this.spGameStart.node.on(Node.EventType.TOUCH_END, this.touchStartBtn, this)
this.spGameOver.node.on(Node.EventType.TOUCH_END, this.touchStartBtn, this) // 重新开始游戏可以调用不同的方法,这里暂时就调用开始游戏一样的方法
}
增加游戏开始点击事件处理:
touchStartBtn(event: EventTouch){
this.spGameStart.node.active = false
this.gameStatus = GameStatus.Game_Playing // 进入游行进行中状态
this.spGameOver.node.active = false
// 初始化各种状态
for(var i=0;i<3;i++){
var pos = this.pipe[i].getPosition()
pos.x = 170 + 200 * i
var minY = -170
var maxY = 170
pos.y = minY + Math.random() * (maxY - minY)
this.pipe[i].setPosition(pos)
}
var bird = this.node.getChildByName("Bird")
bird.setPosition(new Vec3(0,0,0))
bird.setRotation(new Quat())
}
gameOver函数增加GameOver节点展示:
gameOver(){
this.gameStatus = GameStatus.Game_Over;
this.spGameOver.node.active = true
log('游戏结束')
}
最后在相应的函数增加状态判断,只有Game_Playing状态时小鸟才往前飞,才能进行点击。到这里就能开始游戏啦,至于积分部分我就不写了,可以自行加一下哈哈~
八、最终完整代码
MainController.ts
import { _decorator, BoxCollider2D, Button, Collider2D, Component, Director, director, EventTouch, instantiate, Label, log, Node, Prefab, Quat, Sprite, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
export enum GameStatus{
Game_Ready = 0,
Game_Playing,
Game_Over,
}
@ccclass('MainController')
export class MainController extends Component {
@property(Sprite)
SpBg: Sprite[] = [null, null];
@property(Prefab)
pipePrefab: Prefab = null;
pipe: Node[] = [null, null, null];
gameStatus: GameStatus = GameStatus.Game_Ready;
spGameOver: Sprite = null;
spGameStart: Sprite = null;
protected onLoad(): void {
// 开启点击监听
this.spGameOver = this.node.getChildByName("GameOver").getComponent(Sprite)
this.spGameOver.node.active = false
this.spGameStart = this.node.getChildByName("GameStart").getComponent(Sprite)
this.spGameStart.node.on(Node.EventType.TOUCH_END, this.touchStartBtn, this)
this.spGameOver.node.on(Node.EventType.TOUCH_END, this.touchStartBtn, this)
}
start() {
for (var i = 0; i < 3; i++) {
this.pipe[i] = instantiate(this.pipePrefab) // 实例化水管预制体
this.node.getChildByName("Pipe").addChild(this.pipe[i]) // 找到Pipe节点,在Pipe节点下生成水管,便于控制层级
var pos = this.pipe[i].getPosition()
pos.x = 170 + 200*i // x坐标为水管间隔距离
var minY = -170
var maxY = 170
pos.y = minY + Math.random()*(maxY-minY) // 水管上下高度随机在范围内变化
this.pipe[i].setPosition(pos)
}
}
update(deltaTime: number) {
if(this.gameStatus != GameStatus.Game_Playing)return;
// 背景图向后移动处理
for (var i = 0; i < 2; i++) {
var pos = this.SpBg[i].node.getPosition();
pos.x -= 1.0;
if (pos.x <= -288) {
pos.x = 288;
}
this.SpBg[i].node.setPosition(pos)
}
// 水管向后移动并生成新水管
for(var i=0;i<3;i++){
var pos = this.pipe[i].getPosition()
pos.x -= 1.0 // 向后移动处理
if(pos.x <= -170){ // 移出画面后变为新的水管
pos.x = 430
var minY = -170
var maxY = 170
pos.y = minY + Math.random()*(maxY - minY)
}
this.pipe[i].setPosition(pos)
}
}
gameOver(){
this.gameStatus = GameStatus.Game_Over;
this.spGameOver.node.active = true
log('游戏结束')
}
touchStartBtn(event: EventTouch){
this.spGameStart.node.active = false
this.gameStatus = GameStatus.Game_Playing // 进入游行进行中状态
this.spGameOver.node.active = false
// 初始化各种状态
for(var i=0;i<3;i++){
var pos = this.pipe[i].getPosition()
pos.x = 170 + 200 * i
var minY = -170
var maxY = 170
pos.y = minY + Math.random() * (maxY - minY)
this.pipe[i].setPosition(pos)
}
var bird = this.node.getChildByName("Bird")
bird.setPosition(new Vec3(0,0,0))
bird.setRotation(new Quat())
}
}
BirdController.ts
import { _decorator, BoxCollider2D, Collider2D, IPhysics2DContact, Canvas, Collider, Component, EventTouch, log, Node, Quat, Contact2DType } from 'cc';
const { ccclass, property } = _decorator;
import { MainController,GameStatus } from './MainController';
@ccclass('BirdController')
export class BirdController extends Component {
@property(Canvas)
Canvas: Canvas = null;
speed: number = 0;
mainController: MainController = null;
protected onLoad(): void {
// 监听屏幕点击事件
this.Canvas.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
this.mainController = this.Canvas.getComponent(MainController)
}
start() {
let collider = this.getComponent(BoxCollider2D)
if (collider) {
collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
}
}
update(deltaTime: number) {
if(this.mainController.gameStatus != GameStatus.Game_Playing)return;
this.speed -= 0.05; // 每次速度减少
var pos = this.node.getPosition();
pos.y += this.speed; // 效果是每次加速一点点往下冲
this.node.setPosition(pos);
// 小鸟头朝向的角度处理
var angle = (this.speed / 2) * 30
if (angle >= 30) {
angle = 30;
}
this.node.setRotation(Quat.fromEuler(new Quat(), 0, 0, angle));
}
/**
* 点击屏幕时触发
*/
onTouchStart(event: EventTouch) {
if(this.mainController.gameStatus != GameStatus.Game_Playing)return;
this.speed = 2;
}
/**
* 只在两个碰撞体开始接触时被调用一次
* @param selfCollider 指的是回调脚本的节点上的碰撞体
* @param otherCollider 指的是发生碰撞的另一个碰撞体
* @param contact 碰撞主要的信息, 位置和法向量, 带有刚体的本地坐标来,
*/
onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
this.mainController.gameOver() // 调用游戏结束函数
this.speed = 0
}
}