自制游戏【跳跃牢烟】
案例解析
案例需求,点击鼠标控制白块左右。
资源管理器部分
在body创建一个2d精灵用作玩家。
在地下在创建一个2d精灵用来代表地面。
在body下挂在脚本。
全部脚本如下
(在二次进行复刻时候,发现把代码复制上去无法运行,原来cocos的引入,需要每次在vscode里面挨个打的时候将会自动添加引入,如果直接甩给它,他不会引入任何模块。)
import { _decorator, Component, EventMouse, Input, input, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('NewComponent')
export class NewComponent extends Component {
start() {
input.on(Input.EventType.MOUSE_DOWN,this.IKUN,this)
}
IKUN(event:EventMouse){
if(event.getButton()==2){
this.jump(2)
}
if(event.getButton()==0){
this.jump(0)
}
}
jump(kun:number){
if(kun==2){
const cur = this.node.position;
this.node.setPosition(cur.x+40,cur.y,cur.z);
}
if(kun==0){
const cur = this.node.position;
this.node.setPosition(cur.x-40,cur.y,cur.z);
}
}
// protected iikun():void{
// input.off(Input.EventType.MOUSE_DOWN,this.IKUN,this);
// }
update(deltaTime: number) {
}
}
分段拆解
导入模块
typescript
import { _decorator, Component, EventMouse, Input, input, Node } from 'cc';
const { ccclass, property } = _decorator;
从 'cc' 模块导入了 _decorator、Component、EventMouse、Input、input、Node 等类将
_decorator 中的 ccclass 和 property 解构出来,用于后续的类装饰。
start 方法
typescript
start() {
input.on(Input.EventType.MOUSE_DOWN, this.IKUN, this)}
start 方法在组件所在的节点被激活时调用一次。
使用 input.on 方法监听鼠标按下事件 Input.EventType.MOUSE_DOWN,当鼠标按下时,会调用 this.IKUN 方法,并且将当前组件实例 this 作为上下文传递。
IKUN 方法
typescript
IKUN(event: EventMouse) {
if (event.getButton() == 2) {
this.jump(2)
}
if (event.getButton() == 0) {
this.jump(0)
}}
· 当 MOUSE_DOWN 事件发生时,输入系统会调用 IKUN 方法,并将生成的 EventMouse 事件对象作为参数传递给 IKUN 方法,这就是 event 的来源。
· IKUN 方法接收一个 EventMouse 类型的参数。
· 为什么值是eventmouse呢,因为传过来的值是一个鼠标事件(MOUSE_DOWN)嘛
event: EventMouse)也就是说,这个形参的值,是一个EventMouse类型的(鼠标类型)
· 通过 event.getButton() ,意思是获取event里面的getbutton()的值。
· 获取鼠标按下的按钮,2 表示鼠标右键,0 表示鼠标左键。
· 根据按下的按钮不同,调用 this.jump 方法,并传递相应的参数 2 或 0。
jump 方法
typescript
jump(kun: number) {
if (kun == 2) {
const cur = this.node.position;
this.node.setPosition(cur.x + 40, cur.y, cur.z);
}
if (kun == 0) {
const cur = this.node.position;
this.node.setPosition(cur.x - 40, cur.y, cur.z);
}}
· jump 方法接收一个 number 类型的参数 kun。
· 如果 kun 为 2,表示鼠标右键按下,将当前节点的 x 坐标增加 40。
· 如果 kun 为 0,表示鼠标左键按下,将当前节点的 x 坐标减少 40。
· 通过 this.node.position 获取当前节点的位置,然后使用 this.node.setPosition 方法设置新的位置。
update 方法
update 方法在每一帧都会被调用,参数 deltaTime 表示从上一帧到当前帧的时间间隔。
· 该方法目前为空,没有实现任何功能。
注释代码
// protected iikun():void{// input.off(Input.EventType.MOUSE_DOWN,this.IKUN,this);// }
· 这段代码被注释掉了,如果取消注释,iikun 方法会取消监听鼠标按下事件 Input.EventType.MOUSE_DOWN,参数与 input.on 方法相对应,用于移除之前添加的事件监听器
2d视野下的调整
项目设置-》项目数据-》设计宽度(高度)
视野跟随指定物体移动
在层级管理器里面把摄像机(camera)放在指定物体下面
渲染画布外面的部分
Canvas-》属性检查器-》xx.canvas->align canvas wlith screen
小方块缓慢的移动
凭借上述的代码,可以实现小方块的移动,但这样的结果过于僵硬,我们可以通过别的方法来让小方块自然一些。
首先看全部的代码,随后进行分段讲解。
import { _decorator, Component, EventMouse, Input, input, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('NewComponent')
export class NewComponent extends Component {
private starikun = false;
private _jumptime = 0.2;
private _ttime = 0;
private _ikunsp =0;
start() {
input.on(Input.EventType.MOUSE_DOWN,this.IKUN,this)
}
IKUN(event:EventMouse){
if(event.getButton()==2){
this.jump(2)
}
if(event.getButton()==0){
this.jump(-2)
}
}
jump(kun:number){
this.starikun = true;
this._ttime = 0;
this._ikunsp = kun*20/this._jumptime;
}
// protected iikun():void{
// input.off(Input.EventType.MOUSE_DOWN,this.IKUN,this);
// }
update(dt: number) {
if(this.starikun=true){
this._ttime+=dt;
if(this._ttime>this._jumptime){
this.starikun=false;
}else{
const cupikun =this.node.position;
this.node.setPosition(cupikun.x+this._ikunsp*dt,cupikun.y,cupikun.z);
}
}
}
}
第一段,基本上没有区别,单纯的增加了几个变量
import { _decorator, Component, EventMouse, Input, input, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('NewComponent')
export class NewComponent extends Component {
private starikun = false;
private _jumptime = 0.2;
private _ttime = 0;
private _ikunsp =0;
start() {
input.on(Input.EventType.MOUSE_DOWN,this.IKUN,this)
}
IKUN(event:EventMouse){
if(event.getButton()==2){
this.jump(2)
}
if(event.getButton()==0){
this.jump(-2)
}
}
starikun 用来判断到时候该不该移动
_jumptime是小方块移动的时候占据多少时间
Titime是当作计时器使用
Ikunsp用来当作速度
第二段
把之前让小方块移动的代码删除了,换做更为复杂的代码,但是这让小方块看起来流畅很多。
这将会使用内置函数update,所以jump函数的作用不再是直接控制移动了。
jump(kun:number){
this.starikun = true;
this._ttime = 0;
this._ikunsp = kun*20/this._jumptime; }
starikun 更改为true,下面有用
_ttime 表示时间为0,初始化时间。
this._ikunsp = kun*20/this._jumptime; }
是求速度的,因为速度等于路程/时间
最后一段是利用update函数,因为updata函数每一帧都会执行一次,借助特性来使达到想要的缓慢到达目标地点的方块方法。
update(dt: number) {
if(this.starikun=true){
this._ttime+=dt;
if(this._ttime>this._jumptime){
this.starikun=false;
}else{
const cupikun =this.node.position;
this.node.setPosition(cupikun.x+this._ikunsp*dt,cupikun.y,cupikun.z);
}
}
}
update(dt: number) ,把每针的时间定义为dt。
if(this.starikun=true),starikun变量等于true时候执行。下面全部的函数。
this._ttime+=dt;使计时器增加,每次增加一帧的时间。
下面进行逻辑判断,如果计时器的时间大于要求的跳跃时间,那么starikun改变,也就进行运动了。
如果没有大于,那么每次都使当前时间乘速度,让小球位移。因为一帧一帧移动,所以看起来会缓和许多,不会过于生硬。
但这时候通过编辑器预览发现小方块的移动并非是完全与速度与时间的乘积相等。
我推测是帧率的时间问题,可能大个几毫秒或者少于几毫秒。
具体的解决措施是将小方块最后移动到该移动的位置。
如何让小方块移动的更加精准
在start函数上方定义两个私有变量。一个是用来保存当前位置,一个是用来保存小方块最终移动位置。
private _zdp = new Vec3;
private _zd = new Vec3;
_zdp是最终的位置
_zd用来存储当前位置。
Vec3是一个存储三维数据的一个变量。
New是对象与类的基本知识。
在jump函数的部分让_zd获取当前的位置,让_zdp获取最终位置。
this._zd = this.node.position;
//this.node.getPosition(this._zd);
这两种方法都可以。
一个是把当前的位置赋值给变量。
一个是把node当前的位置通过getposition传递给this.zd。
之后是推测最终位置,把最终地点的坐标传递给_zdp
this._zdp = new Vec3(this._zd.x+jumpp,this._zd.y,this._zd.z);
其中的jumpp是把固定的距离变成了一个变量,这样想更改距离的时候就不用挨个去更改了,只要直接改一下jumpp的数值就好。
在jump函数第一行定义了一个 const jumpp = kun*20;
其中 this._zdp = new Vec3(this._zd.x+jumpp,this._zd.y,this._zd.z);也可以用vec.add来替换。
Vec3.add(this._zdp,this._zd,new Vec3(jumpp,0,0));
这段代码表示,将第二位与第三位参数相加并传递给第一个参数。
然后就是等函数整体运行之后把位置改变为_zdp位置。
在updata函数里面的 if(this._ttime>this._jumptime)的末尾加上一个
this.node.setPosition(this._zdp);
解决重复按下鼠标导致位置发生偏移的问题。
在jump函数下,第一行增加一句
if(this.starikun)return
意思是starikun为true的话后面代码直接不执行,涉及到if基本知识和return基本知识。
跳跃动画
添加跳跃动画组件
首先选中要添加跳跃动画的组件,在下方动画编辑器里面为其创建,或者在选中组件的时候在右侧添加动画组件(animation)
创建动画剪辑资源
在下方动画编辑器之中创建动画剪辑资源,这时候观测到有两个明显属性。
1节点列表2属性列表
选中某个节点或子节点,对其该节点的某个或多个属性进行修改。
属性列表关键帧
剪辑基础知识,过。
通过简单捅咕即可学会创建一个动画,接下来使代码与动画连接。
不要忘记保存动画,在场景编辑器的那个。
这时候如果选择已经加载好动画的节点,为其animation属性勾选上加载后跳动,那将会在加载此节点的时候执行一次默认动画。
代码部分。
引入动画模块,使其节点可以加载刚才的动画。
@property(Animation)
public bodyiAnim:Animation =null;
@property是一个装饰器(decorator),它主要用于将一个类的成员变量进行属性化处理。
属性化后就可以在节点的部分直接看见。
Animation 在这里是一个类型说明,表明了被装饰的成员变量的类型。
这时候观测该节点的父节点,单击后查看右侧部分,就会神奇的发现多了个节点属性。
注意,在引入Animation容易产生错误,因为有同名的其他,引入cc下的才是正确的。
扩展
在上述学习之中已经了解@property是cocos之中将一个类的成员变量进行属性化处理。尝试自定义一个进行尝试。
代码部分
@property(Number)
myNumber: number = 42;
返回父节点进行观测,发现新增一个属性,名称为myNumber,数值是42。
在写完代码后,返回cocos属性面板,这时候bodyiAnim=null;,将存在动画的子节点拖入该区域。
当你把节点拖入 bodyiAnim 属性时,实际上是在告诉 Cocos:“我希望 bodyiAnim 变量引用这个节点上的动画组件。
在跳跃时候播放动画
在jump函数里面,也就是准备跳的时候执行动画的播放即可。
代码部分
This.bodyiAnim.play(“动画名”);
这时候运行就可以跳跃了。
接下来目的是创建一个新的动画,让跳一步和两步的动画分开,并在程序中进行判断,如果是一步调用那个动画,两部调用那个动画。
首先是如何创建一个新的动画,在选中节点后,在下方动画编辑器之中进行,动画剪辑-》下标-》新建剪辑动画.
代码部分。
在之前This.bodyiAnim.play(“动画名”);进行修改,使其增加一个判断,如果是左键播放什么动画,右键播放什么动画。
if(kun==2){
this.bodyani.play("animation");
}
if(kun==4){
this.bodyani.play("animation-001");
}
这时候发现动画的时间和移动的时间又不一样了,那么可以选择更改动画,让动画时间与移动的时间相等,或者让移动时间等于动画的时间。
这里根据教程使用的是让移动时间等于动画时间。
代码部分
const aniname= kun==2?'animation':'animation-001';
const anistate=this.bodyani.getState(aniname);
this.jumptime=anistate.duration;
const aniname= kun==2?'animation':'animation-001';利用三元表达式,如果kun的值是2,那么返回值animation,否则返回值animation-001。并把值返回给常量aniname。
getState 通常可以被理解为 “获取状态”。翻译一下就是获取bodyani下的aniname的动画的状态。
最后这行代码是把找到的动画状态(anistate)里的 duration 属性的值取出来,存储在 this.jumptime 中。
在此时,前面写的if分支if(kun==2){可以删除了,换成this.bodyani.play(aniname);就可以了。
随机生成地图
生成预制体
在层级管理器之中将地面拖入到资源管理器之中,这会生成一个预制体。
创建一个新的脚本和一个新的空节点,将脚本挂在到新的节点上。
在脚本内需要对预制体进行引用,还要创建一个参数用来保存到底生成多少格子,将他们属性化是一个很好的方法。
代码部分
@property(Prefab)
public boxprefab:Prefab =null;
@property
public roalength =50;
将资源管理器之中准备好的预制体地面放进刚才创建的节点的刚刚代码创建的属性值之上。
然后创建一个枚举类型。用来表示方块应该是那种状态。
代码部分
enum bolkeType{
BT_none,
BT_white}
enum 是枚举(Enumeration)的缩写,它是一种数据类型,用于定义一组具名的常量。在这个例子中,bolkeType 是枚举的名称。
BT_none 和 BT_white 是 bolkeType 枚举中的成员。
使用 enum 定义枚举时,这些成员会被自动赋值为数字。BT_none 会被赋值为 0,BT_white 会被赋值为 1。
创建一个 _road变量,让其存储刚才定义的枚举类型。
代码部分
private _road:bolkeType[]=[];
bolkeType[]:这是变量的类型,表明 _road 是一个数组,该数组的元素类型是 之前定义的bolkeType 枚举类型里面的类型。这意味着该数组中的每个元素都应该是 bolkeType 枚举中定义的成员,如 BT_none 或 BT_white。
在start函数里面调用一个自创的函数,具体操作将写在这里。
代码部分
start() {
this.generate();
}
generate(){
this.node.removeAllChildren();
This._road=[];
this._road.push(bolkeType.BT_white);
}
this.node.removeAllChildren();销毁该节点下所有子节点。防止被多次调用。This._road同理
push() 是数组的一个内置方法,它的作用是向数组的末尾添加一个元素。
循环生成道路方块。
接下来通过代码来生成地面方块,通过for循环可以简单构成下面代码,这个循环存在于generate之中。
for(let i=1;i<this.roalength;i++){
}
然后通过生成一个随机的数值,来确定生成哪一个地板块。
for(let i=1;i<this.roalength;i++){
//Math.round(Math.random());
this._road.push( Math.random()<0.5?0:1);
}
Math.random() 是一个 JavaScript 内置的方法,它会生成一个范围在 [0, 1] 之间的随机浮点数,即大于或等于 0 且小于 1 的随机数。
Math.random() < 0.5? 0 : 1:
这是一个三元运算符。它会检查 Math.random() 的结果是否小于 0.5。如果小于 0.5,表达式的值为 0;如果大于或等于 0.5,表达式的值为 1。
Math.round():
这也是 JavaScript 中的一个内置函数,它的作用是将一个数字四舍五入为最接近的整数。对于 Math.round(x),如果 x 的小数部分小于 0.5,则将 x 向下取整;如果 x 的小数部分大于或等于 0.5,则将 x 向上取整。
但要考虑如果连续生成两个块为空,那么将无法进行,所以要进行逻辑判断,如果上一个为空块,则这次必须是白块。
代码部分
for(let i=1;i<this.roalength;i++){
if(this._road[i-1]==bolkeType.BT_none){
this._road.push(bolkeType.BT_white);
}else{
this._road.push( Math.random()<0.5?0:1);
}
}
上述代码是生成了小方块应该出现的类型,但是并没有真正的去生成方块,那么通过应该出现的类型来简单生成方块。
首先通过for循环去遍历,然后通过判断是否应该生成白块来确定方块的生成。
for( let j=1;j<this.roalength;j++){
if(this._road[j]==bolkeType.BT_white){
const box =instantiate(this.boxprefab);
box.setParent(this.node);
box.setPosition(j*40,0,0);
}
}
if (this._road[j] == bolkeType.BT_white):
this._road[j]:从 this._road 数组中取出索引为 j 的元素。
bolkeType.BT_white:这是之前定义的枚举类型 bolkeType 中的一个枚举成员。
这里的条件判断是为了检查 this._road[j] 是否等于 bolkeType.BT_white,如果相等,则执行 if 语句块内的代码。
this.boxprefab 是一个预制体,类似于一个魔法模具。
instantiate 函数根据 this.boxprefab 创建一个新的对象,这个对象存储在 box 中,如同用模具做出一个新的物品。
setParent(this.node) 就像是给这个 “饼干” 找一个 “盘子”。this.node 就是这个 “盘子”,把 box 放在 this.node 下面,就表示让 box 成为 this.node 的子节点
box.setParent(this.node):将 box 的父节点设置为 this.node。这在游戏开发>游戏开发或节点树结构中很常见,用于组织和管理对象的层次结构。
box.setPosition(j * 40, 0, 0);:
box.setPosition(j * 40, 0, 0):调用 box 的 setPosition 方法,将 box 的位置设置为 (j * 40, 0, 0)。这里的 j * 40 表示 x 轴上的位置,根据 j 的不同会有所变化,而 y 和 z 轴的位置始终为 0。 就是在给这个 “饼干” 找到一个合适的位置。
创建ui
在场景的根节点处,再次创建一个画布节点,用于创建ui等,与之前的游戏画布节点呈现兄弟关系。
在ui画布下面创建一个空节点,在次空节点上创建一个精灵图。
找一个图片当作背景,对其进行拉伸处理,在精灵图属性之中,UItransform里面content sizi属性进行更改。
对精灵图的精灵属性进行修改,让其type属性更改为 SLICED 将鼠标悬停到type属性上的三个点,系统会说明该属性的用处。
创建一个开始按钮,一个积分器的文本label。
渲染问题
当两个摄像机模块存在时候,会导致一个物体被渲染两次,造成非常鬼畜的样子。
选择摄像机,更改其属性,Visibility。这将会告诉摄像机,该摄像机渲染那些类型的层。
选择画布,更改其属性,node属性的layer属性。告诉画布,其下的子节点包括自身是属于什么layer。
将游戏的那区域改为defxxx层,记得勾选连同子节点一起更改。
就是让一个摄像机渲染ui,一个渲染游戏,画布和摄像机为一个组合,这时候总共两个组合,画布下的所有东西定义为一个图层组,另一个也是,让摄像机分别只渲染自己组的画布。
返回之前写地面的脚本。
定义一个枚举类型,已用来保存游戏状态。
代码部分
enum Gamestate{
GS_ui,
GS_PLAYER
}
如果是ui状态,那么点击不应该触发小方块跳动,如果是play状态,那么那些ui组件应该隐藏。
设置一个方法,方法内判断当前状态,是Gs-ui还是gs_player
代码部分
start() {
this.setcurstate(Gamestate.GS_ui);
}
setcurstate(vl:Gamestate){
if(vl===Gamestate.GS_ui){
this.generate();
}else if(vl===Gamestate.GS_PLAYER){
}
}
这里更改了原先的start()方法,原先的start()方法会直接调用 this.generate();来生成地面。
现在的逻辑是判断是什么状态,再决定生不生成地面。
那么在vl===Gamestate.GS_ui的时候,需要让其禁用掉鼠标的监听。
返回让小方块跳动的代码部分。
取消掉star()开始函数里面的一切。
创建一个公共类型的函数,如果函数内的值为真,注册监听函数,如果为假,取消掉注册函数。
返回地面生成的脚本,添加一个引入。这里将会引入小方块脚本。
然后看之前写的setcurstate函数部分,通过下面的方法来控制另一个脚本的值。
两者有唏嘘差别,具体写时候具体理解。
将层级管理器的小方块节点引入。
之后处理对于ui界面的引用
将层级管理器的ui部分拖拽进去。
更改之前的setcurstate部分,使其让ui部分隐藏
在按钮部分,选择其属性,将cilick events设置为1。
这时候可以设置激活那个节点的哪个属性的那个方法。
让其调用地面脚本的属性。
在小方块的脚本上创建一个公共变量,默认值为0,在每次跳动时候加上跳动的步数,然后创建一个监听,在每次小方块结束移动时候。在使用地面脚本进行监听该事件。
创建一个公共变量
public fenshu:number=0;
每次跳动时候加上跳动的步数
this.fenshu+=xx;
然后创建一个监听事件,让地面脚本去监听这个fenshu
this.node.emit("jumpend",this.fenshu);
这个是放在update函数里面的跳跃结束时候的下面。
if(this.stazz==true){
if(this.tti>this.time){
this.stazz=false;
this.node.setPosition(this.zzwz);
this.node.emit("jumpend",this.fenshu);
this.node.emit() 是 Cocos Creator 中的一个方法,用于在当前节点上触发一个自定义事件。
事件的名称是 "jumpend",后面跟着的是传递给事件处理程序的参数 this.fenshu。
然后回到地面脚本,在start监听上面的参数。
因为之前引入过小方块脚本,所以直接监听即可。
this.player.node.on("jumpend",this.onjumpend,this);
this.player.node 表示获取 this.player 所引用的节点。
然后,通过调用 on 方法,将一个名为 jumpend 的事件绑定到该节点上。当这个节点上触发 jumpend 事件时,就会调用 this.onjumpend 方法。
创建一个onjumpend方法。
onjumpend(value:number){
this.ui_labl.string=value.toString();
}
ui_labl之前没有,是新创建的,创建一个公共变量ui_labl,用于引用ui界面的文本,该文本作为积分器使用。
返回cocos的层级管理器,创建一个label。
然后回到代码部分,把.ui_labl属性化,将label拖入进去。
这样,游戏开始时候进行一个监听,监听的代码又会改变label节点的内容,每次小方块停止移动都会发送一个信号,然后被监听到,然后监听的代码又会改变label节点的内容......
当完成上述代码,游戏只差最后一个是否失败,以及是否通关的判定了,大段代码如下。
这时候发现路并没有回归原点,通过在小方块里面创建一个公用变量,将当前节点的位置更改为0.将之前传参的分数,也设置为0.
public endfang(){
this.node.setPosition(0,0,0);
this.fenshu=0;
}
然后在地面脚本的判断游戏状态(判断当前是ui还是游戏的那个)里面进行一个调用,让每次生成前先归零参数。
setcurstate(vl:Gamestate){
if(vl===Gamestate.GS_ui){
this.player.endfang();
这时候,每次死亡后发现计数器并不会直接为0,实际上参数已经为0了,但是没有传输过去,所以需要一个手动的更新。
setcurstate(vl:Gamestate){
if(vl===Gamestate.GS_ui){
this.player.endfang();
this.ui_labl.string='0';