游戏编程模式-命令模式(Command)

command:基于事件驱动的命令模式实现 异步派发 C++11实现 《C++元编程与通用设计模式实现》专著获市场与学术双重认可。该书基于工业实践编写,由清华大学出版社出版后已加印,并被中国国家图书馆及35所高校图书馆收录。书中详细解析了命令模式的事件驱动实现,包括dispatcher、mainLoop等核心组件的设计原理与线程安全机制,通过分层架构实现模块化与可扩展性,为构建高效事件驱动系统提供了实用解决方案。作者开源代码仓库可通过gitee搜索"galaxy_0"获取。 阅读详情

@TOC

命令模式(Command)

GoF的描述生涩难懂

命令模式是我最喜欢的模式之一。在我编写的绝大多数的程序、游戏或者其他东西中,最终在某些地方都会使用到它。当我在合适的地方使用它,它能把一些棘手的代码清晰地整理解开。对于这样出色的模式,GoF对此有个让人生涩难懂的描述:

将请求封装为对象,从而使用户能够以不同的请求对客户端进行参数化、对请求进行排队或记录,并支持可撤销的操作。

这是个很糟糕的描述。首先,这个描述歪曲了想要表达的含义。特别是"客户端"这个词,抛开软件领域,“客户端"可以代表一个人,比如一个与你做生意的人。据我所知,人类不能被"参数化”。

然后,这个描述的剩余部分只是你可以/可能使用该模式的概念清单。除非你用例存在概念恰好在上述清单中,否则没有太大启发意义。

命令是具体化的方法调用

"具体化"表示将某个概念转化为一段数据(一个对象:你可以将其放入变量、传递给函数等等)。因此,通过说命令模式是“具体化的方法调用”,我的意思是它是一个包装在对象中的方法调用。

取决于你使用的编程语言和习惯,具体而言这听起来很像一个“回调函数”、“一等函数”、“函数指针”、“闭包”或“部分应用函数”等概念,而且它们确实都在同一个范围内。GoF后来表示:

命令是一种替代回调的面向对象方案

配置输入映射

在每个游戏程序中,都有一段代码读取原始的用户输入,比如按键、键盘输入、鼠标点击等事件。这段代码检查每个事件同时转译为对应的动作,如下举例所示:

void InputHandler::handleInput()
{
  if (isPressed(BUTTON_X)) jump();
  else if (isPressed(BUTTON_Y)) fireGun();
  else if (isPressed(BUTTON_A)) swapWeapon();
  else if (isPressed(BUTTON_B)) lurchIneffectively();
}

这个函数在游戏循环的每帧中被调用。如果我们固定了用户输入与游戏动作的映射,那么这段代码是可行的。但是绝大多数游戏是允许用户配置输入映射的。

为了支持输入映射可配置的操作,那么意味着上述的中每个条件分支下的对应行为是可变的(举例:当满足isPressed(BUTTON_X)这个条件后,转译的动作是可变的。)。"可变的"在软件中往往暗示着一个变量,变量是可变的,因此我们可以用一个变量去代表游戏动作,我们可以用个抽象的基类代表一个游戏动作, 如下所示:

class Command
{
public:
  virtual ~Command() {}
  virtual void execute() = 0; //execute是个子类必须实现的虚函数,用来执行最终的动作
};

那么我们可以创建对应不同游戏动作的子类实现:

class JumpCommand : public Command
{
public:
  virtual void execute() { jump(); }
};

class FireCommand : public Command
{
public:
  virtual void execute() { fireGun(); }
};

// You get the idea...

在InputHandler中,为了进行配置输入映射,变为了如下所示的代码结构:

class InputHandler
{
public:
    // 使用函数指针执行动作
    handleInput()
    {
        if (isPressed(BUTTON_X)) buttonX_->execute();
        else if (isPressed(BUTTON_Y)) buttonY_->execute();
        else if (isPressed(BUTTON_A)) buttonA_->execute();
        else if (isPressed(BUTTON_B)) buttonB_->execute();
    }
    // 增加输入映射配置的功能
    void reMapX(Command* action)
    {
        this->buttonX_ = action;// 对X进行配置
    };
    void reMapY(Command* action)
    {
        this->buttonY_ = action;// 对Y进行配置
    };
    // 为了A和B等按键映射...
  // Methods to bind commands...

private:
    Command* buttonX_; //函数指针
    Command* buttonY_;
    Command* buttonA_;
    Command* buttonB_;
};

引入角色

上述的实现动作的函数,比如jump()、fireGun()等,这些函数是无参函数,说明只能对固定的对象执行对应的固定动作。如果引入角色,那么可想而知,玩家角色和Npc角色的jump、fireGun等动作肯定是有所区别的。因此Command基类将引入传参actor:

class Command
{
public:
  virtual ~Command() {}
  virtual void execute(GameActor& actor) = 0;
};

动作的实现函数也将发现变化:

class JumpCommand : public Command
{
public:
  virtual void execute(GameActor& actor)
  {
    actor.jump(); // jump函数的最终实现被绑定到了actor上
  }
};

因此,handleInput()函数也可以发生改变,返回对应的命令实例对象,如下所示

Command* InputHandler::handleInput()
{
  if (isPressed(BUTTON_X)) return buttonX_;
  if (isPressed(BUTTON_Y)) return buttonY_;
  if (isPressed(BUTTON_A)) return buttonA_;
  if (isPressed(BUTTON_B)) return buttonB_;
  // 未实现,返回NULL
  return NULL;
}

因为handleInput函数没有传入具体的角色对象,所以上述代码没有立刻执行实际的命令。这带来一个优点:我们可以延后进行命令的执行。如下所示:

Command* command = inputHandler.handleInput();
if (command)
{
  command->execute(actor);
}

我们所举的这个例子中,actor代表被用户输入驱动的玩家角色。我们通过Command对象实现了决策和动作的分离,因此我们自然也可以想到游戏中的AI角色(Npc),如果被控角色是一样的,那么AI角色跟玩家角色的不同之处就是驱动命令的来源问题,因此我们可以用下述的示意图统一表示这种模式

💡 批注:为什么特意使得命令可序列化,成为流呢?我们可以把命令流通过网络传输。因此我们能够把玩家输入通过网络传输到另外的机器上,并且重现它。这在多人网络游戏中是很重要的功能。

控制者
命令流
被控角色
AI
命令流
Npc
玩家
命令流
玩家角色

撤销和复现

💡 批注:为了同上述小结内容衔接,保持内容连贯;这个小结内容不使用原作者的原文字翻译,全部是个人理解,同时修改了示例代码。

撤销和复现是很多软件和游戏中存在的功能。对于软件而言,我们能够经常看到撤销和复现的功能,比如windows的画图(可以撤销绘图操作)、windows的记事本(可以撤销最近的文字)、游戏设计软件、工业软件等。对于游戏而言,特别是策略游戏,比如塔防/棋类游戏中,在玩家的决策回合,玩家可以指定某个单位前往地图上的某个坐标上,玩家可能会不小心或者错误指定位置,那么游戏通常是会提供撤销功能,以此让该单位恢复原来的位置。那么我们该如何实现撤销功能呢?我们依然是对Command这个基类进行修改,如下所示:

class Command
{
public:
  virtual ~Command() {}
  virtual void execute(GameActor& actor) = 0;
  virtual void undo(GameActor& actor) = 0; // 增加撤销操作接口
};

我们以棋类游戏(比如象棋)的棋子移动的撤销功能举例,那么这个棋子移动的命令功能如何是西安呢?如下所示:

class MoveUnitCommand : public Command
{
public:
  MoveUnitCommand(int x, int y)
  : xBefore_(0),
    yBefore_(0),
    x_(x),
    y_(y)
  {}

  virtual void execute(GameActor& actor)
  {
    // 记录棋子单位的先前的、旧的位置,以便能够恢复之前的位置
    xBefore_ = actor.x();
    yBefore_ = actor.y();

    actor.moveTo(x_, y_);
  }

  virtual void undo(GameActor& actor)
  {
    // 恢复之前的位置
    actor.moveTo(xBefore_, yBefore_);
  }

private:
  int xBefore_, yBefore_; // 棋子先前的位置
  int x_, y_; // 本次命令的位置
};

那么我们如何使用命令流进行撤销undo和redo操作呢?使用下述的代码进行说明,其中使用Control-Z代表撤销,Control-Y代表恢复


int undoOrRedo(std::vector<Command*> commandStream, 
               int current_index,
               GameActor& actor){
    /*
    commandStream: 命令流
    current_index: 代表当前执行的命令位置
    actor: 操控对象
    return: 返回更新后的命令位置
    */
    if (isPressed(BUTTON_Control_Z) && current_index >= 0) {
        // 执行撤销操作,返回上一个命令的位置
        commandStream[current_index]->undo(actor);
        return current_index - 1;
    } else if (isPressed(BUTTON_Control_Y) && current_index >= -1 && current_index < commandStream.size() - 1) {
        // 执行下一个命令的动作,同时返回下一个命令的位置
        int redo_index = current_index + 1;
        commandStream[redo_index]->execute(actor);
        return redo_index;
    }
    return current_index;
}

命令模式在不同编程语言下的差异

💡 批注:这个小结内容不使用原作者的原文字翻译(原文标题为Classy and Dysfunctional?),全部是个人理解,同时修改了示例代码。

上述的代码示例中均使用了C++进行展示,因为C++对闭包函数的支持不够友好,因此我们使用了class类进行替代;如果你所使用的编程语言支持闭包函数,那么完全可以用闭包函数进行替代。因此可以这么理解:命令模式是在没有闭包的语言中模拟闭包的一种方法。

对于上述例子,加入我们使用JavaScript进行编写,那么我们可以如下实现:

function makeMoveUnitCommand(x, y) {
  var xBefore, yBefore;
  return {
    execute: function(actor) {
      xBefore = actor.x();
      yBefore = actor.y();
      actor.moveTo(x, y);
    },
    undo: function(actor) {
      actor.moveTo(xBefore, yBefore);
    }
  };
}
C++命令模式解析:从原理到游戏开发实践 命令模式是行为设计模式的一种,它将请求封装为独立对象,实现操作调用者与执行者的解耦。这种模式的核心价值在于支持请求排队、日志记录和撤销操作,特别适合需要灵活控制操作流程的场景。在C++中,命令模式通常通过抽象命令接口、具体命令类、调用者和接收者四个组件实现。游戏开发是其典型应用领域,如技能系统、AI行为队列和输入处理等场景都能从中受益。现代C++特性如std::function和lambda表达式可以简化命令模式的实现,而对象池技术则能优化频繁创建命令对象的性能问题。理解命令模式的工作原理,对于构建可扩展、 阅读详情

相关推荐

Unity 如何使用命令模式

命令模式 游戏中,当你想通过一种通用的方式来存储命令并能够撤消命令时,那么命令模式最有用。 在“示例”中,你将看到一个可以通过GUI按钮移动的简单点。 采取行动后,您可以撤消并重做。我在屏幕上添加了一个简单的OnGUI标签输出,因此您可以看到命令列表,并指出哪个是当前命令。 在此示例中使用了4个类: Command-这是任何命令模式都可以继承的基本结构(抽象类)。您可以在任何项目中使用此类。 MoveCommand-这是从Command类继承的结构,将使用Execute和UnExecute(撤消)。每个移

TxNet 1555

游戏编程模式02-设计模式-命令模式

命令模式把“现在立刻调用某个方法”变成“一个可以保存、传递、排队、撤销的对象”。在游戏里,它很像把玩家输入、AI 决策、回放记录都变成一张张动作卡牌,什么时候执行、由谁执行,可以稍后再决定。命令对象不是越多越好。只有当“调用”本身需要被保存、组合、传输或回滚时,命令对象才开始发挥价值。,按键、角色、行为会绑死在一起。命令模式把调用封装成对象后,可以做到按键重绑定、AI 与玩家共用操作、网络同步、回放、撤销重做。网络同步和回放通常不应保存对象引用,而应保存可序列化的目标 ID、参数和帧号。

qq_37084534的博客 286

弹簧管压力表设计报告

精密机械课程设计 弹簧管压力表设计报告 2020年5月1日 目 录 第一章 设计任务概述... 3 1.1 课程设计目的... 3 1.2 设计任务... 3 1.3 技术指标... 3 第二章 压力表概述... 4 2.1 机械式压力表的组成部分... 4 2.2 工作原理... 4 第三章 压力表设计... 5 3.1 弹簧管参数设计... 5 3.2 曲柄滑块机构设计... ...

weixin_42163746的博客 6451

命令模式简直就是个大宝藏

引言 命令模式是游戏开发中很常用的一种模式。如果只看GoF关于该模式的定义会觉得晦涩懂,不知所云。我对命令模式的简单理解就是对象化的方法调用,我们把一个请求方法进行封装,同时在对应的类中保存这个请求方法在被调用时需要的状态,然后我们可以根据自己的需要在任何时刻对这个被封装的请求就行实例化。更简单点,你可以把它理解成回调的面向对象形式。命令模式的核心就2点:1.请求的封装 2.请求的操作 定义 将“请求”封装成对象,让你可以将客户端的不同请求参数化,并配合队列、记录、...

聊技术,谈人生 495

说说通用报表控件

我们已经开发有2款WINCC报表控件,一种是固定模板,一种是自定义模板。但是它们有个共同特点,就是数据都是来自WINCC的归档数据库。使用WINCC归档数据库的好处是不需要自己写脚本保存数据。但是有些情况下,我们需要自己保存数据,或者第三方系统通过数据表的方式给我们提供数据。那这样的话,我们之前的两款控件就不支持了。正好有个客户有类似需要,于是加了一天班写了个通用报表控件。之所以说是通用,是因为它可以插入任何支持.NET控件的组态软件中,比如WINCC、IFIX等。并且它不依赖于组态软件自己...

HwLib_的博客 3550

dbt+Snowflake实战:数据建模、权限隔离与性能优化全指南

dbt(data build tool)作为现代数据栈的核心建模工具,其与Snowflake的深度协同远不止于SQL编译执行——它本质是将软件工程实践(如版本控制、测试驱动、CI/CD)注入数据交付流程。理解dbt的声明式建模原理与Snowflake的虚拟仓库、Time Travel、TRANSIENT表、聚簇键等底层机制如何咬合,是实现可维护、可审计、低成本数据产品交付的关键。本文聚焦dbt在Snowflake环境中的真实落地挑战,涵盖schema与warehouse的物理映射设计、role级硬隔离策略、

weixin_34389926的博客 312

命令模式在游戏开发中的应用

命令模式的描述: 命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。 意图: 将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。 需求描述:给定两种不同的工具:锤子、刷子;锤子可以用来砸开泥土表层,刷子可...

Sun.ME的博客 1万+

在LangChain中使用DashVector进行高效向量检索

DashVector为需要处理高维向量数据的开发者提供了一种便捷且高效的解决方案。通过将其集成到LangChain生态系统中,您可以轻松实现复杂的语义搜索和数据分析功能。DashVector官方文档LangChain GitHub仓库有关DashVector的详细notebook示例。

sjufgwgfhoia的博客 455

游戏编程模式——命令模式

命令模式 学习从游戏编程模式开始,参考资料是Bob Nystrom的游戏编程模式这本书。当然学习资料是在网上下载的PDF版本,等学完了这本书,一定要买来这本书的纸质版留一个纪念。 GoF是这样定义的: 将一个请求封装为一个对象,从而使你可用不同的请求的客户进行参数化;对请求队列或记录请求日志,以及支持可撤销的操作。 每个游戏都有一些用户输入要处理,比如键盘敲击,鼠标点击等等。例如: 下面是一种简...

TunShiO_o的专栏 692

汇川Easy 301 和MCGS进行Modbus RTU通讯(485)浮点数读取和写入

4DF0001对应寄存器D0,4DF0003对应寄存器D2。注意:要选com2口,com1口是232通讯。

qq_61916672的博客 2901

商城系统全链路压测实战指南

全链路压测是模拟真实用户行为和生产环境流量,验证整个系统在高压下的稳定性和性能表现的关键手段。

monk all the way 1228

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

命令模式:将一个请求封装成一个对象,从而允许你使用不同的请求,队列或者是日志讲客户端参数化,同时支持请求操作的撤销和恢复: 咋一听上去真的不好理解,光说不练假把式,还是动手写东西来的快,既然是比较适用于支持撤销操作的话,那这里改进了个游戏华容道,进行命令模式的演示:cocos creator 2.2.2版本演示 华容道相信大家都玩过,他的命令也不多,就一个移动命令,说到这里就说道了命令模式的一...

lck8989的博客 361

超声波法检测局部放电的原理与实现

摘要:超声波法通过捕捉局部放电产生的声波信号进行检测,系统由超声波传感器、信号调理单元和数据采集系统组成。其核心应用是局放源定位,主要采用时差定位法和声-电联合定位法。该方法具有抗电磁干扰强、定位精确等优点,但也存在信号衰减大、传播路径复杂等缺点,典型应用于变压器、GIS等设备的在线监测与故障诊断。

start_up_go的博客 374

游戏编程模式——命令模式(Command)

命令模式(Command Pattern)通过将命令进行封装,以实现请求者和执行者的解耦,本文将介绍一些命令模式在游戏中的应用。

m0_62210253的博客 1228

在 AS ABAP 7.40 SP05 里把 HANA 存储过程纳入 ABAP 生命周期:AMDP 实战示例与踩坑笔记

文章摘要:AMDP(ABAP Managed Database Procedures)是SAP推出的框架,用于在ABAP中管理数据库端的存储过程或函数,特别适用于HANA数据库。它通过将数据库语言(如SQLScript)嵌入ABAP方法,实现数据处理逻辑的"下推"(code pushdown),减少应用层与数据库间的数据传输。AMDP适用于需要数据库特有能力或大规模数据处理的场景,但应优先考虑Open SQL或ABAP CDS等更简单的方案。开发时需注意数据库类型检查、参数前缀(:)等语

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 225

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

Game Programming Patterns-再探设计模式

游戏编程模式- 再探设计模式,本章目录。

AutoVision (by 仙道菜) 2085

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

入门设计模式(一)命令模式

以下内容参考自《游戏编程模式》一书。 命令模式 可以简单的理解为,“将方法(函数)封装成对象”。 该模式通过将方法解耦,变成可以自由配置的对象,达到了对修改关闭的目的。 成名应用:撤销和重做 例子:玩家自定义按键的功能。 玩家有A、B、X、Y四个键子,一般的写法就是给四个按键分别分配一个方法,比如   input.getKeyDown("A") = Player.Jump(); ...

highmiao的博客 245

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

一天一个设计模式---命令模式

用命令模式来实现一个2D飞行射击游戏,能左右按钮躲避敌人的子弹,切换武器,射击。

记录 1519

《游戏程序设计模式》 1.1 - 命令模式

为什么80%的码农都做不了架构师?>>> ...

weixin_34351321的博客 156

游戏编程模式:命令模式(Part II)

2、Actors的方向         我们刚刚定义的命令类适用于前面的例子,但是它们有很大的局限性。问题在于,它们假设了存在着jump()、fireGun()等等这样的隐含地知道如何找到玩家的角色并像操纵傀儡般操纵它的顶层函数。         这种假定的耦合限制了这些命令的用途。JumpCommand可以使其跳跃的东西只有玩家。让我们来放宽这个限制。我们将我们想要对其发号施令的对象作为一个

哈利_蜘蛛侠的专栏 678

C#游戏框架PureMVC一:最简项目 HelloWorld

Hello World项目总体开发步骤: 开发PureMVC Hello World 项 目总体步骤: 一: 下载框架插件与搭建基础环境。 二: 建立UI界面与定义基础类。 三:初步应用PureMVC。 下载框架插件与搭建基础环境 下载C#版本的PureMVC 框架源码工程。 建立一个空项目。 解压缩 puremvc-csharp-standard-framework-master.zip 文件。 把含有“PureMVC”(注...

Peter的博客 2148

游戏编程模式之命令模式

定义 GoF有个深奥的定义: 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化; 对请求排队或记录请求日志,以及支持可撤销的操作。 命令模式是一种回调的面向对象实现。 作者的简化定义: 命令是具现化的方法调用。 我的理解: 将方法或者类的实例进行传递,实现调用者与命令之间的解耦。 配置输入 如果我们写一段输入,需求如下图。 简单的实现: void InputHandler::handleInput() { if (isPressed(BUTTON_X)) jump(); else

lcy的博客 510

C#--命令模式

文章目录任务描述输入/输出格式:相关知识为什么需要命令模式用命令模式解决问题命令模式结构及角色适合应用场景**任务实现** 任务描述 在植物大战僵尸的游戏中,植物是通过点击界面上“图片按钮”而生产的,请你用命令模式将不同植物的生产请求封装成命令对象。在此例中只有三种植物,分别是豌豆射手,雪花豌豆射手和双管豌豆射手。 输入/输出格式: 输入格式: 输入第一行给出一个字符串“1,3,2,2,1,3”,表示生产植物的数量及类型,其间以半角逗号分隔。(1代表豌豆射手,2代表雪花豌豆射手,3代表双管豌豆射手)

生夏夏的博客 684

【游戏设计模式】之四 《游戏编程模式》全书内容提炼总结

本文的Github版本:QianMo/Reading-Notes/《游戏编程模式》读书笔记这是一篇超过万字读书笔记,总结了《Game Programming Patterns》(中译版《游戏编程模式》)一书中所有章节与内容的知识梗概。我们知道,游戏行业其实一直很缺一本系统介绍游戏编程进阶技巧的书籍,而《游戏编程模式》的出现,正好弥补了这一点。之前已经有提到过,不同于传统的出版方式,这本书是网络出版...

weixin_43701019的博客 183

《游戏编程模式》读书笔记 命令模式

命令模式下的代码则是这样:if (玩家按下空格) jumpCommand.excute(),其中jumpCommand是一个Command类的实例,Command类都有excute方法,并且不同的子类分别有自己的实现。这一例中,jumpCommand可能就是一个JumpCommand类的实例,而JumpCommand类是Command类的一个子类,在它的excute方法中,调用了jump()这一方法,从而让命令模式下这段代码最终也调用到了jump()方法。例如,在玩家按下空格时,让玩家操作的角色跳跃。

snake_knife的博客 277

php new command,PHP_php设计模式 Command(命令模式),<?php /** * 命令模式 * * 将一 - phpStudy...

php设计模式 Command(命令模式)/*** 命令模式** 将一个请求封装为一个对象从而使你可用不同的请求对客户进行参数化,对请求排除或记录请求日志,以及支持可取消的操作*/interface Command{public function execute();}class Invoker{private $_command = array();public function setCom...

weixin_30760143的博客 97
上一篇: [Onnx简化库深度剖析] OnnxSimplifier和OnnxOptimizer解读-(4)
下一篇: 游戏编程模式-享元模式(Flyweight)
慷仔
博客等级 码龄9年 88粉丝 46原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值