游戏设计模式之-命令模式

命令模式:将一个请求封装成一个对象,从而允许你使用不同的请求,队列或者是日志讲客户端参数化,同时支持请求操作的撤销和恢复:

咋一听上去真的不好理解,光说不练假把式,还是动手写东西来的快,既然是比较适用于支持撤销操作的话,那这里改进了个游戏华容道,进行命令模式的演示:cocos creator 2.2.2版本演示

华容道相信大家都玩过,他的命令也不多,就一个移动命令,说到这里就说道了命令模式的一个缺点就是会导致类膨胀,假如有成百上千个命令,那么继承的代码就会很多,这样确实是不好处理。

好了废话不多说,首先讲一讲最重要的一个类就是Command了,对所有的命令都继承这个抽象类,其他的子类只需要实现它的方法就ok了,

import { Action } from "./Action";
import Handler from "./Handler";

export default abstract class Command {

    constructor() {

    }
    
    // 执行命令
    public abstract exec();
    // 撤销操作
    public abstract undo();
    // 重做指令
    public abstract redo();
}

这里最重要的方法就是exec方法和undo方法,分别对应着执行命令和撤销命令操作,有了这些东西,它需要一个真正干活的家伙,有请MoveCommand闪灵登场:

import Command from "./Command";
import { Action } from "./Action";
import Handler from "./Handler";
import Hero from "../Hero";
enum Direction {
    TOP,
    BOTTOM,
    LEFT,
    RIGHT
}
export default class MoveCommand implements Command {

    private moveNode: cc.Node;
    private map: number[][];
    private dir: number;
    private pointMap: cc.Vec2[][];

    constructor(moveNode: cc.Node = null,map: number[][] = null,dir: number = -1,pointMap: cc.Vec2[][]) {
        this.moveNode = moveNode;
        this.map = map;
        this.dir = dir;
        this.pointMap = pointMap;
    }
    
    // 执行
    public exec() {
        if(this.dir === Direction.TOP) {
            this.move2Top();
        }
        if(this.dir === Direction.BOTTOM) {
            this.move2Bottom();
        }
        if(this.dir === Direction.LEFT) {
            this.move2Left();
        }
        if(this.dir === Direction.RIGHT) {
            this.move2Right();
        }
    }
    // 撤销操作
    public undo() {
        if(this.dir === Direction.TOP) {
            this.move2Bottom();
        }
        if(this.dir === Direction.BOTTOM) {
            this.move2Top();
        }
        if(this.dir === Direction.LEFT) {
            this.move2Right();
        }
        if(this.dir === Direction.RIGHT) {
            this.move2Left();
        }
    }
    // 重做
    public redo() {

    }
    private computePosition(nodeCom: Hero,colArr: number[],rowArr: number[]): void {
        colArr = nodeCom.getColArr();
        // nodeCom.setColArr[]
        let sumX: number = 0;
        let sumY: number = 0;
        for(let m = 0; m < colArr.length; m++) {
            for(let n = 0; n < rowArr.length; n++) {
                let positionItem: cc.Vec2 = this.pointMap[rowArr[n]][colArr[m]];
                if(positionItem) {
                    sumX += positionItem.x;
                    sumY += positionItem.y;
                }
            }
        }
        let x = sumX / (rowArr.length * colArr.length);
        let y = sumY / (rowArr.length * colArr.length);
        this.moveNode.x = x; 
        this.moveNode.y = y;

        // this.moveNode = null;
    }
    // 向上走
    private move2Top(): void {
        // 改变地图的模型数据
        let nodeCom: Hero = <Hero>this.moveNode.getComponent("Hero");
        let rowArr: number[] = nodeCom.getRowArr();
        let colArr: number[] = nodeCom.getColArr();
        let maxRow: number = Math.max(...rowArr);
        let minRow: number = Math.min(...rowArr);
        for(let i = 0; i < colArr.length; i++) {
            this.map[maxRow + 1][colArr[i]] = 1;
            this.map[minRow][colArr[i]] = 0;
        }
        let targetRowArr: number[] = rowArr.map((value) => {
            return value + 1;
        });
        nodeCom.setRowArr(targetRowArr);
        this.computePosition(nodeCom,colArr,targetRowArr);
    }
    // 向下走
    private move2Bottom(): void {
        let nodeCom: Hero = <Hero>this.moveNode.getComponent("Hero");
        let rowArr: number[] = nodeCom.getRowArr();
        let colArr: number[] = nodeCom.getColArr();
        let maxRow: number = Math.max(...rowArr);
        let minRow: number = Math.min(...rowArr);
        for(let i = 0; i < colArr.length; i++) {
            this.map[maxRow][colArr[i]] = 0;
            this.map[minRow - 1][colArr[i]] = 1;
        }
        let targetRowArr: number[] = rowArr.map((value) => {
            return value - 1;
        });
        nodeCom.setRowArr(targetRowArr);
        this.computePosition(nodeCom,colArr,targetRowArr);
    }
    // 向左走
    private move2Left(): void {
        // 改变地图的模型数据
        let nodeCom: Hero = <Hero>this.moveNode.getComponent("Hero");
        let rowArr: number[] = nodeCom.getRowArr();
        let colArr: number[] = nodeCom.getColArr();
        for(let i = 0; i < rowArr.length; i++) {
            for(let j = 0; j < colArr.length; j++) {
                this.map[rowArr[i]][colArr[j]] = 0;
                this.map[rowArr[i]][colArr[j] - 1] = 1;
            }
        }
        let targetcolArr: number[] = colArr.map((value) => {
            return value - 1;
        });
        nodeCom.setColArr(targetcolArr);
        this.computePosition(nodeCom,colArr,rowArr);
    }
    // 向右走
    private move2Right(): void {
        // 改变地图的模型数据
        let nodeCom: Hero = <Hero>this.moveNode.getComponent("Hero");
        let rowArr: number[] = nodeCom.getRowArr();
        let colArr: number[] = nodeCom.getColArr();
        for(let i = 0; i < rowArr.length; i++) {
            for(let j = 0; j < colArr.length; j++) {
                this.map[rowArr[i]][colArr[j]] = 0;
                this.map[rowArr[i]][colArr[j] + 1] = 1;
            }
        }
        let targetcolArr: number[] = colArr.map((value) => {
            return value + 1;
        });
        nodeCom.setColArr(targetcolArr);
        this.computePosition(nodeCom,colArr,rowArr);
    }
}

这个类保存了是那个节点执行了哪个方法(对应它的dir属性)他是直接改变了游戏中的map对象,这样在撤销操作的时候只需要执行它相反的方法就行了,

问题来了,这个到底怎么封装这个命令出来呢?这里思考几分钟不要看下面的答案哈哈

答案就是:命令处理类 CommandHandler

它的内部应用这两个栈,undo栈,redo栈

我们每执行一个移动命令都实例化一个对象出来,放到undo栈中,等撤销的时候从这个undo栈中进行取命令执行完事,代码如下:

/**
 * 
 * 
 * 命令模式处理器
 * 
 */
import { Action } from "./Action";
import Command from "./Command";

export default class CommandHandler {

    private undoStack: Command[];
    private redoStack: Command[];
    private action: Action;

    constructor() {
        this.undoStack = [];
        this.redoStack = [];
    }
    // 处理
    public handle(action: Action,command?: Command) {
        // 执行
        if(action === Action.EXEC) {
            command.exec();
            this.undoStack.push(command);
            this.redoStack = [];
        }
        // 撤销
        if(action === Action.UNDO && this.undoStack.length > 0) {
            let command: Command = this.undoStack.pop();
            command.undo();
            this.redoStack.push(command);
        } else {
            console.log("栈里面没有命令了");
        }
        // 重做
        if(action === Action.REDO && this.redoStack.length > 0) {
            let command: Command = this.redoStack.pop();
            command.redo();
            this.undoStack.push(command);
        }
    }

}

在游戏的类game.ts文件中初始化这个处理类:

private commandHandler: CommandHandler = null;
    async start () {
       this.commandHandler = new CommandHandler();
       // 设置有限状态机状态
       this.curState = GameState.INIT;
       this.nodeDirection = Direction.RIGHT;

       await this.init();
    }

当按钮点击撤销的时候执行撤销操作:

private btnEvent(e: cc.Event,data: any): void {
        switch(data) {
            case "next":
                break;
            case "pre":
                break;
            case "restart":
                this.restart();
                break;  
            case "undo":
                // 执行悔棋逻辑
                this.commandHandler.handle(Action.UNDO);
                break;      
        }
    }

游戏主逻辑记录了当前的移动方向,然后就可以发送命令开始执行了:

set curDir(dir: Direction) {
        if(this.currentMoveNode) {
            switch(dir) {
                case Direction.TOP:
                    this.nodeDirection = Direction.TOP;
                    this.commandHandler.handle(Action.EXEC,new MoveCommand(this.currentMoveNode,this.map,Direction.TOP,this.pointMap));
                    break;
                case Direction.BOTTOM:
                    this.nodeDirection = Direction.BOTTOM;
                    this.commandHandler.handle(Action.EXEC,new MoveCommand(this.currentMoveNode,this.map,Direction.BOTTOM,this.pointMap));
                    break;    
                case Direction.LEFT:
                    this.nodeDirection = Direction.LEFT;
                    this.commandHandler.handle(Action.EXEC,new MoveCommand(this.currentMoveNode,this.map,Direction.LEFT,this.pointMap));
                    break;
                case Direction.RIGHT:
                    this.nodeDirection = Direction.RIGHT;
                    this.commandHandler.handle(Action.EXEC,new MoveCommand(this.currentMoveNode,this.map,Direction.RIGHT,this.pointMap));
                    break;        
            }
        }
    }

大功告成,打完收工

猜你喜欢

转载自blog.csdn.net/lck8989/article/details/105317828