设计模式-命令模式-以游戏快捷键为例

超级链接: Java常用设计模式的实例学习系列-绪论

参考:《HeadFirst设计模式》


1.关于命令模式

命令模式是一种行为型模式。

命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。

本文以游戏快捷键为场景来学习命令模式

  • 在MMORPG游戏中,游戏操作有:角色前进、角色跳跃、释放技能:火球术、释放技能:潜行术、打开背包界面、打开技能界面等等。
  • 需求一:通过按键进行快捷操作。例如:按下空格键,则角色跳跃;按下W键,则角色前进。
  • 需求二:可以替换快捷键。例如:默认角色跳跃的快捷键是空格,用户可以自定义为回车键。
  • 需求三:可以自定义宏命令,一个宏命令可以进行多个操作。例如:按下按键Q,则依次进行:角色前进、释放技能:潜行术、释放技能:火球术。

2.按键

无论如何实现三个需求,我们先来实现按键本身。

游戏按键:枚举:KeyEnum

/**
 * <p>按键(部分)</P>
 *
 * @author hanchao
 */
public enum KeyEnum {
    /**
     * 空格键
     */
    KEY_SPACE,

    /**
     * 按键:回车
     */
    KEY_ENTER,

    /**
     * 按键:A
     */
    KEY_A,

    /**
     * 按键:B
     */
    KEY_B,

    /**
     * 按键:V
     */
    KEY_V,

    /**
     * 按键:W
     */
    KEY_W,

    /**
     * 按键:1
     */
    KEY_1,

    /**
     * 按键:2
     */
    KEY_2,

    /**
     * 按键:无
     */
    KEY_NULL
}

2.游戏操作

无论如何实现三个需求,我们先来实现游戏操作本身。

游戏操作:角色相关:Role

/**
 * <p>游戏角色</P>
 *
 * @author hanchao
 */
@Slf4j
public class Role {

    /**
     * 前进
     */
    public static void forward() {
        log.info("角色前进");
    }

    /**
     * 跳跃
     */
    public static void jump() {
        log.info("角色跳跃");
    }
}

游戏操作:技能相关:SkillHandler

/**
 * <p>技能管理器</P>
 *
 * @author hanchao
 */
@Slf4j
public class SkillHandler {

    /**
     * 释放技能-五火球神术
     */
    public static void fireBall() {
        log.info("释放技能:火球术...酝酿1秒钟...砰! 砰!! 砰!!!");
    }

    /**
     * 释放技能-潜行
     */
    public static void sneak() {
        log.info("释放技能:潜行术...蒙面...抛出烟雾弹...不见了!");
    }
}

游戏操作:界面相关:UIHandler

/**
 * <p>游戏界面</P>
 *
 * @author hanchao
 */
@Slf4j
public class UiHandler {
    /**
     * 打开背包界面
     */
    public static void showPack() {
        log.info("打开背包界面,背包中现在有:2个面包,3棵草药,5块矿石...");
    }

    /**
     * 打开技能界面
     */
    public static void showSkill() {
        log.info("打开技能界面,已掌握的技能有:暗影之舞,剑刃风暴,神圣之光...");
    }
}

3.实现方式:if-else/switch

实现思路:

  • 遍历所有按键,然后进行相应的处理。这种实现方式,能够满足第一条需求。
  • 对于第二、三条需求,实现起来比较麻烦。

4.实现方式:命令模式

实现命令模式的关键在于:将命令/请求封装为一个对象

在我们的例子中,控制角色向前释放技能:火球术打开背包界面等等,都是一种命令/请求

这些命令可以抽象为一个抽象类:命令Command

4.1.命令抽象:Command

/**
 * <p>命令:执行、撤销</P>
 *
 * @author hanchao
 */
public interface Command {
    /**
     * 执行命令
     */
    void execute();
}

4.2.普通命令实现

命令实现:角色跳跃:RoleJumpCommand

角色前进:RoleForwardCommand实现方式类似。

/**
 * <p>命令:跳跃</P>
 *
 * @author hanchao
 */
public class RoleJumpCommand implements Command {

    /**
     * 执行命令
     */
    @Override
    public void execute() {
        Role.jump();
    }
}

命令实现:释放技能:火球术:ReleaseFireBallCommand

释放技能:潜行术:ReleaseSneakCommand实现方式类似。

/**
 * <p>命令:释放技能:火球术</P>
 *
 * @author hanchao
 */
public class ReleaseFireBallCommand implements Command {

    /**
     * 执行命令
     */
    @Override
    public void execute() {
        SkillHandler.fireBall();
    }
}

命令实现:打开背包界面:ShowPackCommand

打开技能界面:ShowSkillCommand实现方式类似。

/**
 * <p>命令:打开背包界面</P>
 *
 * @author hanchao
 */
public class ShowPackCommand implements Command {

    /**
     * 执行命令
     */
    @Override
    public void execute() {
        UiHandler.showPack();
    }
}

4.3.默认命令实现

命令实现:默认命令:DefaultCommand

可能存在这种情况:某个按键并未关联任何命令,如果按下此键,应该不会产生任何效果。

为了统一处理上述情况,定义一种默认命令,这样就不必专门去进行非空判断。

/**
 * <p>默认命令</P>
 *
 * @author hanchao
 */
@Slf4j
public class DefaultCommand implements Command {

    /**
     * 执行命令
     */
    @Override
    public void execute() {
        log.info("什么也没有发生");
    }
}

4.4.宏命令实现

命令实现:宏命令:MacroCommand

所谓宏命令,就是一次按键,产生多个游戏操作。其实,宏命令本身也是一种命令。

为了实现多个游戏操作的需求,我们可以通过集合来实现。

/**
 * <p>宏命令</P>
 * <p>
 * 需求三:可以自定义宏命令,一个宏命令可以进行多个操作。
 *
 * @author hanchao
 */
@AllArgsConstructor
public class MacroCommand implements Command {

    /**
     * 宏命令列表
     */
    private List<Command> commandList;

    /**
     * 执行命令
     */
    @Override
    public void execute() {
        //依次执行命令
        for (Command command : commandList) {
            command.execute();
        }
    }
}

4.5.三个需求的实现

下面编写客户代码。

首先,定义按键管理器``KeyManager`,主要关注点:按键初始化:设置默认按键。

/**
 * <p>调用者:按键管理器</P>
 *
 * @author hanchao
 */
@Slf4j
public class KeyManager {

    /**
     * 假设共计32种游戏操作
     */
    private static final int MAX_SIZE = 32;

    /**
     * 快捷键列表
     */
    private static Map<KeyEnum, Command> commandMap = new HashMap<>(MAX_SIZE);

    static {
        //按键初始化:设置默认按键
        commandMap.put(KeyEnum.KEY_W, new RoleForwardCommand());
        commandMap.put(KeyEnum.KEY_SPACE, new RoleJumpCommand());
        commandMap.put(KeyEnum.KEY_B, new ShowPackCommand());
        commandMap.put(KeyEnum.KEY_V, new ShowSkillCommand());
        commandMap.put(KeyEnum.KEY_1, new ReleaseFireBallCommand());
        commandMap.put(KeyEnum.KEY_2, new ReleaseSneakCommand());
        commandMap.put(KeyEnum.KEY_NULL, new DefaultCommand());
    }
}

4.5.1.需求一的实现

需求一:通过按键进行快捷操作。例如:按下空格键,则角色跳跃;按下W键,则角色前进。

在按键管理器``KeyManager中定义按键press()`方法,实现需求一。

    /**
     * 需求一:通过按键进行快捷操作。
     */
    public static void press(KeyEnum key) {
        //如果旧的按键为空,则置为空按键
        if (Objects.isNull(key)) {
            key = KeyEnum.KEY_NULL;
        }

        log.info("按下了「{}」", key.name());
        //执行此按键
        Command command = commandMap.get(key);
        if (Objects.isNull(command)){
            command = new DefaultCommand();
        }
        command.execute();
        log.info("----------");
    }

4.5.2.需求二的实现

需求二:可以替换快捷键。例如:默认角色跳跃的快捷键是空格,用户可以自定义为回车键。

在按键管理器``KeyManager中定义设置自定义按键setCustomKey()`方法,实现需求二。

   /**
     * 需求二:可以替换快捷键。
     */
    public static void setCustomKey(Command command, KeyEnum oldKey, KeyEnum newKey) {
        //如果旧的按键为空,则置为空按键
        if (Objects.isNull(oldKey)) {
            oldKey = KeyEnum.KEY_NULL;
        }

        //如果新的按键为空,则置为空按键
        if (Objects.isNull(newKey)) {
            newKey = KeyEnum.KEY_NULL;
        }


        log.info("将「{}」操作的快捷键进行替换:{} --> {}", command.getClass().getSimpleName(), oldKey.name(), newKey.name());

        //将旧按键绑定到默认操作
        if (!Objects.equals(oldKey, KeyEnum.KEY_NULL)) {
            commandMap.put(oldKey, new DefaultCommand());
        }

        //将新按键绑定到当前操作
        commandMap.put(newKey, command);
    }

4.5.3.需求三的实现

需求三:可以自定义宏命令,一个宏命令可以进行多个操作。例如:按下按键Q,则依次进行:角色前进、释放技能:潜行术、释放技能:火球术。

其实之前已经实现了,在章节4.4.中,实现了宏命令,宏命令也是命令。

4.6.运行效果

测试代码:GameClientDemo

/**
 * <p>客户端</P>
 *
 * @author hanchao
 */
public class GameClientDemo {

    public static void main(String[] args) {
        //默认按键
        KeyManager.press(KeyEnum.KEY_W);
        KeyManager.press(KeyEnum.KEY_SPACE);
        System.out.println("==============================================================================================================");

        //将角色跳跃的快捷键进行替换
        KeyManager.setCustomKey(new RoleJumpCommand(), KeyEnum.KEY_SPACE, KeyEnum.KEY_ENTER);
        KeyManager.press(KeyEnum.KEY_SPACE);
        KeyManager.press(KeyEnum.KEY_ENTER);
        System.out.println("==============================================================================================================");

        //自定义宏命令:角色前进、释放技能:潜行术、释放技能:火球术,并将其绑定在按键A
        KeyManager.press(KeyEnum.KEY_A);
        List<Command> commandList = Lists.newArrayList(
                new RoleForwardCommand(), new ReleaseSneakCommand(), new ReleaseFireBallCommand()
        );
        KeyManager.setCustomKey(new MacroCommand(commandList), null, KeyEnum.KEY_A);
        KeyManager.press(KeyEnum.KEY_A);
    }
}

测试结果

2019-07-28 11:47:50,919  INFO - 按下了「KEY_W」 
2019-07-28 11:47:50,923  INFO - 角色前进 
2019-07-28 11:47:50,923  INFO - ---------- 
2019-07-28 11:47:50,923  INFO - 按下了「KEY_SPACE」 
2019-07-28 11:47:50,923  INFO - 角色跳跃 
2019-07-28 11:47:50,923  INFO - ---------- 
==============================================================================================================
2019-07-28 11:47:50,923  INFO - 将「RoleJumpCommand」操作的快捷键进行替换:KEY_SPACE --> KEY_ENTER 
2019-07-28 11:47:50,923  INFO - 按下了「KEY_SPACE」 
2019-07-28 11:47:50,923  INFO - 什么也没有发生 
2019-07-28 11:47:50,923  INFO - ---------- 
2019-07-28 11:47:50,923  INFO - 按下了「KEY_ENTER」 
2019-07-28 11:47:50,923  INFO - 角色跳跃 
2019-07-28 11:47:50,923  INFO - ---------- 
==============================================================================================================
2019-07-28 11:47:50,923  INFO - 按下了「KEY_A」 
2019-07-28 11:47:50,923  INFO - 什么也没有发生 
2019-07-28 11:47:50,924  INFO - ---------- 
2019-07-28 11:47:50,941  INFO - 将「MacroCommand」操作的快捷键进行替换:KEY_NULL --> KEY_A 
2019-07-28 11:47:50,941  INFO - 按下了「KEY_A」 
2019-07-28 11:47:50,942  INFO - 角色前进 
2019-07-28 11:47:50,943  INFO - 释放技能:潜行术...蒙面...抛出烟雾弹...不见了! 
2019-07-28 11:47:50,943  INFO - 释放技能:火球术...酝酿1秒钟...砰! 砰!! 砰!!! 
2019-07-28 11:47:50,943  INFO - ---------- 

5.关于命令操作的取消

在命令模式的定义中,有这么一句话:对请求排队或记录请求日志,以及支持可取消的操作。

当然在游戏快捷键这个例子中,状态取消是不合适的,总不能让已经释放的火球术再返回来吧。

但是在有些场景是有必要存在的,比方说word文档的回撤操作等。

那么,如何实现取消操作呢?

  1. 在抽象命令类中定义取消cancel()操作
  2. 在所有命令实现类中实现取消cancel()操作
  3. 在客户端类中,定义已执行命令栈Stack,当一个命令被执行时,将其入栈;当一个命令被取消时,将其出栈。

6.实际应用

  • 熔断器HystrixCommand
  • CQRS

7.总结

最后以UML类图来总结本文的游戏快捷键场景以及命令模式
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值