- 业务需求:2.5D效果,三列式经典跑酷游戏,有金币/障碍物,角色可跳跃,跃起时不触发金币/障碍物,固定时长流程,结束计分
- 限制:纯2D素材,纯2D实现
项目需求限制导致很难使用物理碰撞系统/距离检测机制的传统方式解决问题,但三列式设计给了一些可趁之机,正所谓
“游戏的艺术就在于视觉欺骗”
简单示意图如上图。
可以发现,如果我们关注单个物品的生命周期,其从出现-->与玩家产生交互-->出画面,每段的时间都是固定的
即可以抽象为 第0秒实例化 -->第x秒开始进入与角色交互状态-->第x+a秒退出与角色交互状态-->第x+a+b秒销毁的模式
也就是说物品的逻辑层和视觉层是可以做到完全分离的
我们只需在角色的update周期中读取当前列是否有正在生效的物体和跳跃状态就可以实现逻辑层需求了
而视图层则直接将2d素材从上到下从小到大进行缓动,即可伪造出2.5d效果
方案确定,开码
首先简单做一个角色状态机
export interface ICharaState {
SM: CharacterSM;
charaSpine: sp.Skeleton;
clipName: string;
stateIn: Function;
stateOut: Function;
canGoTo: string[];
lockTime: number;
}
interface定义,包括状态机实例引用,状态动画,动画片段名,入/出状态回调,可切换至的state列表,状态锁定时间
export class RunningState implements ICharaState {
charaSpine: sp.Skeleton;
clipName: string;
SM: CharacterSM;
constructor(sm: CharacterSM, charaSpine: sp.Skeleton, clipName: string) {
this.SM = sm;
this.charaSpine = charaSpine;
this.clipName = clipName;
}
canGoTo: string[] = ['strafingLeft', 'strafingRight', 'pausing', 'jumping', 'failed'];
stateIn = () => {
this.SM.unscheduleAllCallbacks();
this.SM.lockState(this.lockTime);
this.charaSpine.addAnimation(0, this.clipName, true);
};
stateOut = () => {
this.charaSpine.clearTracks();
};
lockTime = 0;
}
export class JumpingState implements ICharaState {
charaSpine: sp.Skeleton;
clipName: string;
SM: CharacterSM;
constructor(sm: CharacterSM, charaSpine: sp.Skeleton, clipName: string) {
this.SM = sm;
this.charaSpine = charaSpine;
this.clipName = clipName;
}
canGoTo: string[] = ['running', 'failed'];
stateIn = () => {
this.SM.unscheduleAllCallbacks();
this.SM.lockState(this.lockTime);
SoundManager.instance.playSound('jump');
this.SM.scheduleOnce(() => {
this.SM.switchState('running');
}, 0.6);
this.charaSpine.addAnimation(0, this.clipName, false);
};
stateOut = () => {
this.charaSpine.clearTracks();
};
lockTime = 0.6;
}
把需要的状态一一定义出来,包括跑,跳,左右移动,暂停,失败。此处举例两个,跳状态通过进入回调时添加定时任务回到跑状态即可自动落地。设置canGoTo即可进行操作锁,此处即跳跃中不可左右移动
export class CharacterSM extends Component {
@property(sp.Skeleton)
charaSkeleton!: sp.Skeleton;
public states: { [key: string]: ICharaState } = {};
public currentState!: ICharaState;
public stateLocked: boolean = false;
public lockState(duration: number) {
this.stateLocked = true;
this.scheduleOnce(() => {
this.stateLocked = false;
}, duration);
}
public switchState(stateName: string): boolean {
console.log(`trying to sw