参考游戏编程模式
并发状态机
我们决定给英雄拿枪的能力。 当她拿着枪的时候,她还是能做她之前的任何事情:跑动,
跳跃,跳斩,等等。 但是她在做这些的同时也要能开火。
如果我们执着于FSM,我们需要翻倍现有状态。 对于每个现有状态,我们需要另一个她持
枪状态:站立,持枪站立,跳跃,持枪跳跃, 你知道我的意思了吧。
多加几种武器,状态就会指数爆炸。 不但增加了大量的状态,这也增加了大量的冗余: 持
枪和不持枪的状态是完全一样的,只是多了一点负责射击的代码。
问题在于我们将两种状态绑定——她做的和她携带的——到了一个状态机上。 为了处理所有
可能的组合,我们需要为每一对组合写一个状态。 修复方法很明显:使用两个单独的状态
机。
如果她在做什么有n个状态,而她携带了什么有m个状态,要塞到一个状态机中,
我们需要n × m个状态。使用两个状态机,就只有n + m个。
我们保留之前记录她在做什么的状态机,不用管它。 然后定义她携带了什么的单独状态机
。 Heroine将会有两个“状态”引用,每个对应一个状态机,就像这样:
···
class Heroine
{
// 其他代码……
private:
HeroineState* state_;
HeroineState* equipment_;
};
···
为了便于说明,她的装备也使用了状态模式。 在实践中,由于装备只有两个状态
,一个布尔标识就够了。
当英雄把输入委托给了状态,两个状态都需要委托:
···
void Heroine::handleInput(Input input)
{
state_->handleInput(this, input);
equipment_->handleInput(this, input);
}
···
功能更完备的系统也许能让状态机销毁输入,这样其他状态机就不会收到了。 这
能阻止两个状态机响应同一输入。
每个状态机之后都能响应输入,发生行为,独立于其它机器改变状态。 当两个状态集合几
乎没有联系的时候,它工作得不错。
在实践中,你会发现状态有时需要交互。 举个例子,也许她在跳跃时不能开火,或者她在
持枪时不能跳斩攻击。 为了完成这个,你也许会在状态的代码中做一些粗糙的if测试其他
状态来协同, 这不是最优雅的解决方案,但这可以搞定工作。
···
// 定义角色状态
enum CharacterState {
Idle,
Moving,
Attacking,
Ducking,
Jumping,
}
// 定义角色装备状态
enum EquipmentState {
None,
Gun,
}
// 定义角色类
class Character extends cc.Component {
private _stateStack: CharacterState[] = [CharacterState.Idle]; // 角色状态栈
private _equipmentStack: EquipmentState[] = [EquipmentState.None]; // 角色装备状态栈
private _isFiring: boolean = false; // 是否正在开火
private _moveSpeed: number = 100; // 移动速度
private _fireInterval: number = 0.5; // 开火间隔时间
private _fireTimer: number = 0; // 开火计时器
constructor() {
super();
}
update(dt: number) {
this._fireTimer += dt;
}
// 切换角色状态方法
private changeCharacterState(newState: CharacterState) {
if (this._stateStack[this._stateStack.length - 1] === newState) {
return;
}
this._stateStack.push(newState);
switch (newState) {
case CharacterState.Idle:
this.stopMoving();
break;
case CharacterState.Moving:
this.startMoving();
break;
case CharacterState.Attacking:
this.startAttacking();
break;
case CharacterState.Ducking:
this.startDucking();
break;
case CharacterState.Jumping:
this.startJumping();
break;
}
}
// 结束当前角色状态方法
private endCurrentCharacterState() {
const currentState = this._stateStack.pop()!;
switch (currentState) {
case CharacterState.Idle:
break;
case CharacterState.Moving:
this.stopMoving();
break;
case CharacterState.Attacking:
this.stopAttacking();
break;
case CharacterState.Ducking:
this.stopDucking();
break;
case CharacterState.Jumping:
this.stopJumping();
break;
}
}
// 切换角色装备状态方法
private changeEquipmentState(newState: EquipmentState) {
if (this._equipmentStack[this._equipmentStack.length - 1] === newState) {
return;
}
this._equipmentStack.push(newState);
switch (newState) {
case EquipmentState.None:
this.stopFiring();
break;
case EquipmentState.Gun:
this.startFiring();
break;
}
}
// 结束当前角色装备状态方法
private endCurrentEquipmentState() {
const currentState = this._equipmentStack.pop()!;
switch (currentState) {
case EquipmentState.None:
break;
case EquipmentState.Gun:
this.stopFiring();
break;
}
}
// 开始移动方法
private startMoving() {
// TODO:播放移动动画等操作
}
// 停止移动方法
private stopMoving() {
// TODO:播放停止动画等操作
}
// 开始攻击方法
private startAttacking() {
if (!this._isFiring && this._fireTimer >= this._fireInterval) {
this._isFiring = true;
this._fireTimer = 0;
// TODO:播放攻击动画等操作,并在攻击结束后调用 stopAttacking 方法停止攻击
}
}
// 停止攻击方法
private stopAttacking() {
this._isFiring = false;
// TODO:停止播放攻击动画等操作
}
// 开始俯卧方法
private startDucking() {
// TODO:播放俯卧动画等操作,并在俯卧结束后调用 stopDucking 方法停止俯卧
}
// 停止俯卧方法
private stopDucking() {
// TODO:停止播放俯卧动画等操作
}
// 开始跳跃方法
private startJumping() {
// TODO:播放跳跃动画等操作,并在跳跃结束后调用 stopJumping 方法停止跳跃
}
// 停止跳跃方法
private stopJumping() {
// TODO:停止播放跳跃动画等操作
}
// 开始开火方法
private startFiring() {
if (!this._isFiring && this._fireTimer >= this._fireInterval) {
this._isFiring = true;
this._fireTimer = 0;
// TODO:播放开火动画等操作,并在开火结束后调用 stopFiring 方法停止开火
}
}
// 停止开火方法
private stopFiring() {
this._isFiring = false;
// TODO:停止播放开火动画等操作
}
// 移动方法
public move(direction: cc.Vec3) {
if (direction.mag() > 0) {
if (this._stateStack[this._stateStack.length - 1] !== CharacterState.Moving) {
this.changeCharacterState(CharacterState.Moving);
}
this.node.position = this.node.position.add(direction.normalize().mul(this._moveSpeed));
} else {
if (this._stateStack[this._stateStack.length - 1] === CharacterState.Moving) {
this.endCurrentCharacterState();
}
}
}
// 切换装备方法
public switchEquipment(equipment: EquipmentState) {
if (this._equipmentStack[this._equipmentStack.length - 1] !== equipment) {
this.endCurrentEquipmentState();
this.changeEquipmentState(equipment);
}
}
// 开火方法
public fire() {
if (this._equipmentStack[this._equipmentStack.length - 1] === EquipmentState.Gun) {
if (this._stateStack[this._stateStack.length - 1] !== CharacterState.Attacking) {
this.changeCharacterState(CharacterState.Attacking);
}
}
}
// 跳跃方法
public jump() {
if (this._stateStack[this._stateStack.length - 1] !== CharacterState.Jumping) {
this.changeCharacterState(CharacterState.Jumping);
}
}
// 俯卧方法
public duck() {
if (this._stateStack[this._stateStack.length - 1] !== CharacterState.Ducking) {
this.changeCharacterState(CharacterState.Ducking);
}
}
// 站起方法
public standUp() {
if (this._stateStack[this._stateStack.length - 1] === CharacterState.Ducking) {
this.endCurrentCharacterState();
}
}
}
···
在上面的代码中,我们定义了一个名为 Character 的角色类,实现了并发状态机。我们使用两个状态栈 _stateStack 和 _equipmentStack 分别表示角色的行为状态和装备状态。在 update 方法中,我们更新开火计时器 _fireTimer 的值。在 changeCharacterState 和 endCurrentCharacterState 方法中,我们根据新状态切换角色的行为,并在需要时播放相应的动画。在 changeEquipmentState 和 endCurrentEquipmentState 方法中,我们根据新装备切换角色的装备状态,并在需要时播放相应的动画。在 move、fire、jump、duck 和 standUp 方法中,我们切换角色的状态,并在需要时播放相应的动画。需要注意的是,在实际开发中,我们还需要根据具体需求进行修改和优化,并添加相应的条件和转换条件。同时,在使用并发状态机时,我们需要小心处理状态之间的交互和冲突问题,并尽可能地保持代码简洁和易于维护。