游戏编程之命令模式

1、什么是命令模式

最近看了《游戏编程模式》这本书,里面介绍了游戏开发时常用的设计模式,当然这些设计模式不只是在开发游戏时才管用,它们同样适用于其他软件开发,适用于各种语言。这里我记录一下自己的学习笔记以及结合unity的使用方法。命令模式是常用的设计模式之一,它的定义是这样:将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。这个定义听起来似乎晦涩难懂,下面用unity游戏开发的例子来说明:
 

2、对客户进行参数化

比如在游戏开发中,产品经理给你提了这样一个需求:按下按键A,控制角色攻击;按下按键B,控制角色奔跑;按下按键C,控制角色跳跃。面对这样一个简单的需求,我们或许会这样写:
void HandleInput()
{
    if (Input.GetKeyDown(KeyCode.A))
    {
        Attack();
    }
    else if (Input.GetKeyDown(KeyCode.B))
    {
        Run();
    }
    else if (Input.GetKeyDown(KeyCode.C))
    {
        Jump();
    }
}
然后,产品经理又提了需求,用户可以自定义按键功能,在很多游戏中都有做这样的功能,为了实现这样的功能,我们应该将这些对Attack()和Run()的调用转化成可以变换的东西,下面用命令模式来重写一下这个功能:
先定义一个抽象类Command作为基类,再定义具体的子类来重写Excute();

public abstract class Command{
    public abstract void Excute(GameActor actor);
}

public class AttackCommand : Command
{
    public override void Excute()
    {
        //攻击逻辑
    }
}

public class RunCommand : Command
{
    public override void Excute()
    {
        //奔跑逻辑
    }
}

public class JumpCommand : Command
{
    public override void Excute()
    {
        //跳跃逻辑
    }
}
在MonoBehaviour的Update函数中,每帧去监听用户输入,并返回对应的command

public class GameControl : MonoBehaviour
{
    private Command buttonA;
    private Command buttonB;
    private Command buttonC;

    private void Start()
    {
        buttonA = new AttackCommand();
        buttonB = new JumpCommand();
        buttonC = new RunCommand();
    }

    private void Update()
    {
        Command cmd = HandleInput();
        if (cmd != null)
        {
            cmd.Excute(actor);
        }
    }

    //处理用户输入
    private Command HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            return buttonA;
        }
        else if (Input.GetKeyDown(KeyCode.B))
        {
            return buttonB;
        }
        else if (Input.GetKeyDown(KeyCode.C))
        {
            return buttonC;
        }
        else
        {
            return null;
        }
    }

}

这样,在按键触发和函数调用中间就加了一层Command,如果要自定义按键功能,直接修改Button对应的Command就行了。现在我们也可以修改一下上面的代码,让我们可以用这套机制去控制任意角色对象,只需将要控制的角色对象传进来即可:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameActor { }

public class Actor1 : GameActor { }

public class Actor2 : GameActor { }

public abstract class Command{
    public abstract void Excute(GameActor actor);
}

public class AttackCommand : Command
{
    public override void Excute(GameActor actor)
    {
        //攻击逻辑
    }
}

public class RunCommand : Command
{
    public override void Excute(GameActor actor)
    {
        //奔跑逻辑
    }
}

public class JumpCommand : Command
{
    public override void Excute(GameActor actor)
    {
        //跳跃逻辑
    }
}


public class GameControl : MonoBehaviour
{
    private Command buttonA;
    private Command buttonB;
    private Command buttonC;

    private GameActor actor;

    private void Start()
    {
        buttonA = new AttackCommand();
        buttonB = new JumpCommand();
        buttonC = new RunCommand();

        actor = new Actor1();
    }

    private void Update()
    {
        Command cmd = HandleInput();
        if (cmd != null)
        {
            cmd.Excute(actor);
        }
    }

    //处理用户输入
    private Command HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            return buttonA;
        }
        else if (Input.GetKeyDown(KeyCode.B))
        {
            return buttonB;
        }
        else if (Input.GetKeyDown(KeyCode.C))
        {
            return buttonC;
        }
        else
        {
            return null;
        }
    }

}

3、支持可撤销的操作

命令模式在需要支持可撤销操作的情况下也能轻松应对,假如我们需要给玩家提供撤销移动操作的功能时,我们可以先把玩家输入产生的command存入栈中(或者其他数据结构),在撤销时,从栈中取出栈顶的Command,再调用该Command的Undo(),就实现了撤销功能(Undo()为撤销方法,与Excute()相反),代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameActor {
    public Transform selfTra;
    public void Move(Vector3 offset)
    {
        selfTra.Translate(offset);
    }
}

public class Actor1 : GameActor { }

public class Actor2 : GameActor { }

public abstract class Command{
    public abstract void Excute(GameActor actor);//执行
    public abstract void Undo(GameActor actor);//撤销
}

public class MoveCommand : Command
{
    public Vector3 moveOffset;
    public MoveCommand(Vector3 offset)
    {
        moveOffset = offset;
    }

    public override void Excute(GameActor actor)
    {
        actor.Move(moveOffset);
    }

    public override void Undo(GameActor actor)
    {
        actor.Move(-moveOffset);
    }
}

public class CommandControl : MonoBehaviour
{
    private Command moveCommand;
    private GameActor actor;
    private Stack<Command> commandStack;

    private void Start()
    {
        moveCommand = new MoveCommand(Vector3.one);
        actor = new Actor1();
        commandStack = new Stack<Command>();
    }

    private void Update()
    {
        Command cmd = HandleInput();
        if (cmd != null)
        {
            commandStack.Push(cmd);
            cmd.Excute(actor);
        }
    }

    //需要撤销操作时调用这个函数
    public void PlayReverse()
    {
        if (commandStack.Count > 0)
        {
            commandStack.Pop().Undo(actor);
        }
    }

    //处理用户输入
    public Command HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            return new MoveCommand(new Vector3(2, 4, 5));
        }
        if (Input.GetKeyDown(KeyCode.B))
        {
            return new MoveCommand(new Vector3(1, 2, 4));
        }
        else 
        {
            return null;
        }
    }

}
上面代码中, 每次产生一个command时就将它存到Stack中,当需要撤销操作时,就取出Stack顶部的command,并执行它的Undo(),按照这种方法,可以实现多重撤销。
 

4、总结

通过上面的例子,我们再看命令模式的定义:将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。现在我们差不多明白了命令模式的用法,它优点很明显,缺点也是有的:第一个优点是类间解耦,调用者和接收者之间没有任何依赖关系,调用者在实现功能时只需调用Command抽象类的Excute方法即可,不需要关注是哪个接收者执行;第二个优点是可扩展性,Command的子类可以很容易地扩展;缺点是如果有大量命令,那么Command的子类将会非常庞大。我们在实际开发中,应该发挥出命令模式的优点,并结合其他模式,减少Command子类庞大的问题。

 

posted @ 2018-12-28 14:56  微束网络  阅读(769)  评论(0编辑  收藏  举报