概述
在肉鸽游戏火热的时候,一直在想如何给用户更多的操作,让玩家通过自己的操作策略体验更多的趣味性。
最开始想到的就是让用户能够主动释放技能,但是主动释放技能似乎对于目前市面上那些需要高频触发技能的肉鸽玩法并不合适,而且仅仅支持主动释放技能,操作策略上还是有些欠缺。
经过深思熟虑后,我们决定采用3D形式,并结合屏幕点击、双击、滑动以及长按等多种交互方式来打造一款以普通攻击为核心机制的游戏。同时,在游戏中加入可由玩家主动控制释放的技能元素。这样的设计不仅能够提升操作过程中的趣味性,还能从视觉上给玩家带来更加沉浸式的体验。
由此,做了一款这样的游戏。

资源已经上架商城
https://store.cocos.com/app/detail/7163
想体验的可以扫描此二维码

本次将教大家如何通过操作手势实现人物控制,以及如何实现人物连击的
单击移动
- 搭建场景,在场景中加入地面以及玩家。
- 在地面和玩家节点上分别加入PlaneCollider和CapsuleCollider
- 接下来我们需要做的就是点击屏幕时,判断屏幕点对应的地面坐标点在人物的哪个方向即可,由于人物在y轴上没有移动,所以只要计算当前点击点xz坐标和人物当前xz坐标后就可以了。
//首先监听点击事件
input.on(Input.EventType.TOUCH_END, this.TouchEnd, this);
//屏幕点击抬起时通过raycast计算出射线与地面的交点,即屏幕点对应的地面点
private GetTouchPoint3d(TouchPoint: Vec2): Vec3 | null {
this.MainCamera.screenPointToRay(TouchPoint.x, TouchPoint.y, this.Ray);
if (PhysicsSystem.instance.raycast(this.Ray)) {
let RayCastResults = PhysicsSystem.instance.raycastResults;
for (let i = 0; i < RayCastResults.length; i++) {
let res = RayCastResults[i];
if (res.collider.node.name == 'TouchCollider') {
return res.hitPoint.clone();
}
}
}
return null;
}
计算出地面点后通过勾股定理计算出人物在xz方向各自的速度,以及人物的朝向即可
private GetMoveDir(TouchPoint3d: Vec3, StartPoint: Vec3 = null): number[] {
if (!StartPoint) {
StartPoint = this.node.getPosition();
}
let deltax = TouchPoint3d.x - StartPoint.x;
let deltaz = TouchPoint3d.z - StartPoint.z;
let dis = Math.sqrt(deltax * deltax + deltaz * deltaz);
let Speedx = deltax / dis * this.Speed;
let Speedz = deltaz / dis * this.Speed;
let Angle = Math.atan2(Speedx, Speedz) * 180 / Math.PI;
return [Speedx, Speedz, Angle];
}
计算出结果后,根据xz的速度更新人物位置
this.node.setRotationFromEuler(new Vec3(0, this.MoveDir[2], 0));
this.UpdatePos(this.node.position.x + this.MoveDir[0] * ratio, this.node.position.y, this.node.position.z + this.MoveDir[1] * ratio);
//这里我设置了一个ratio系数,因为我希望每次点击是由快到慢的移动一段距离,而不是一直朝某个方向移动
双击翻滚
有了单击移动后,双击实现原理就比较简单了,只需要在第一次点击时记录下点击的时间,每次点击的时候计算两次时间的间隔,如果小于一定值,就判定为双击,否则还是单击
//初始化变量
private TouchEndTime: number = 0;
//触发单击事件时,计算时间差
let CurTime = new Date().getTime();
if (CurTime - this.TouchEndTime < 200) {
//这里需要将时间重置,避免下次单击时判断还是双击
this.TouchEndTime = 0;
//双击,翻滚,改变人物的状态
this.ChangePlayerState(PLAYERSTATE.JUMP);
} else {
// 单击,移动,记录单击的时间
this.TouchEndTime = CurTime;
this.ChangePlayerState(PLAYERSTATE.RUN);
}
滑动
滑动判断,并不能直接使用TOUCH_MOVE事件进行处理,需要设定一个阈值,当滑动达到一定距离才会触发,避免误触发
//开启滑动事件监听
input.on(Input.EventType.TOUCH_START, this.TouchStart, this);
input.on(Input.EventType.TOUCH_MOVE, this.TouchMove, this);
//触发TOUCH_START时,记录下当前点击点
this.TouchScreenPoint = event.touch.getLocation();
计算两个点的距离时,由于每隔屏幕分辨率不同,所以是要将屏幕坐标转换到世界坐标
//触发TOUCH_MOVE时,计算当前move的点与TOUCH_START点的距离,超过一点距离就触发滑动
//屏幕坐标转世界坐标
let curpos = new Vec3(event.touch.getLocation().x, event.touch.getLocation().y, 0);
this.UiCamera.screenToWorld(curpos, curpos);
let touchpos = new Vec3(this.TouchScreenPoint.x, this.TouchScreenPoint.y, 0);
this.UiCamera.screenToWorld(touchpos, touchpos);
//超过一定距离则触发滑动
let deltax = curpos.x - touchpos.x;
let deltay = curpos.y - touchpos.y;
if (deltax * deltax + deltay * deltay < 15625) {
return;
}
//滑动
this.MoveDir = this.GetMoveDir(this.GetTouchPoint3d(event.touch.getLocation()), this.TouchStartPoint);
this.node.setRotationFromEuler(new Vec3(0, this.MoveDir[2], 0));
this.ChangePlayerState(PLAYERSTATE.GRAB);
长按
长按逻辑是在update中实现的,触发TOUCH_START时,记录点击的开始时间,在update中计算当前时间和点击开始时间的时间差,大于一定值就触发长按事件。
//TOUCH_START事件记录当前时间
this.TouchStartTime = new Date().getTime();
//update中计算时间差
if (this.TouchStartTime && new Date().getTime() - this.TouchStartTime > 300) {
// 长按
this.ChangePlayerState(PLAYERSTATE.XULI);
}
连击
实现连击,其实就是控制人物状态
//设定一个枚举,记录主角的状态
export enum PLAYERSTATE {
RUN,
ATK1,
ATK2,
ATK3,
ATK4,
ATK5,
}
做一个面片,给人物加一个MeshCollider进行敌人检测,当敌人进入此区域时,就改变人物状态

//开启敌人碰撞监听,监听到敌人进入此区域时,将敌人节点加入列表
this.node.getChildByName('EnemyDetect').getComponent(MeshCollider).on('onTriggerEnter', this.EnemyEnter, this);
EnemyEnter(event: ITriggerEvent) {
this.AroundEnemys.add(event.otherCollider.node);
}
在update中判断,AroundEnemys列表是否有敌人,若有敌人,则根据当前状态切换至下一个状态。
//这里需要设定一个时间,由于状态在update中切换的,所以当用户点击没有敌人的方向时,需要一定时间后才能判断是否再进入攻击状态,否则可能导致始终处于攻击状态
if (this.ActionTime > 0.1 && this.AroundEnemys.size > 0) {
switch (this.CurState) {
case PLAYERSTATE.RUN:
case PLAYERSTATE.ATK5:
this.ChangePlayerState(PLAYERSTATE.ATK1);
break;
case PLAYERSTATE.ATK1:
this.ChangePlayerState(PLAYERSTATE.ATK2);
break;
case PLAYERSTATE.ATK2:
this.ChangePlayerState(PLAYERSTATE.ATK3);
break;
case PLAYERSTATE.ATK3:
this.ChangePlayerState(PLAYERSTATE.ATK4);
break;
case PLAYERSTATE.ATK4:
this.ChangePlayerState(PLAYERSTATE.ATK5);
break;
}
}


