在线书籍《游戏编程模式》简要介绍了命令模式在处理游戏输入中的用法。我试图根据命令模式编写自己的“一刀切”输入处理系统,如下所示。


这是命令模式的正确实现和使用吗?
使用另一种模式(例如观察者模式)会“更好”(更高效,更可维护)吗?
我如何使其适应来自多个控制器的输入?

input_handler.hpp

#ifndef INPUT_HANDLER_HPP
#define INPUT_HANDLER_HPP

#include <map>
#include <vector>
#include <SDL2/SDL.h>
#include "character.hpp"
#include "input_constants.hpp"

class Command
{
    public:
        virtual ~Command() {}
        virtual void execute(Character *character) = 0;
        virtual InputType get_input_type() = 0;
};

class InputHandler
{
    private:
        // Pointers to all commands
        Command *move_up;
        Command *move_down;
        Command *move_left;
        Command *move_right;
        Command *jump;

        std::map <int, Command*> commands;

        // Gameplay context 
        std::map <int, State> state_map;
        std::map <int, Action> action_map;

        bool input_mapping();
        void dispatcher(std::vector<Command*> &command_queue);

        void keydown(SDL_Event &event);
        void keyup(SDL_Event &event);

        bool is_held(int key);
        bool was_pressed(int key);

    public:
        InputHandler();
       ~InputHandler();
        bool fill(std::vector<Command*> &command_queue);
        void configure(int key, Command *command);
};

class MoveUp : public Command
{
    public:
        void execute(Character *character) { character->move_up(); }
        InputType get_input_type() { return STATE; }
};

class MoveLeft : public Command
{
    public:
        void execute(Character *character) { character->move_left(); }
        InputType get_input_type() { return STATE; }
};

class MoveRight : public Command
{
    public:
        void execute(Character *character) { character->move_right(); }
        InputType get_input_type() { return STATE; }
};

class MoveDown : public Command
{
    public:
        void execute(Character *character) { character->move_down(); }
        InputType get_input_type() { return STATE; }
};

class Jump : public Command
{
    public:
        void execute(Character *character) { character->jump(); }
        InputType get_input_type() { return ACTION; }
};

#endif  // INPUT_HANDLER_HPP


input_handler.cpp

#include "input_handler.hpp"

InputHandler::InputHandler()
{

    // Create pointers to all commands (to apply the flyweight pattern)
    move_up = new MoveUp();
    move_down = new MoveDown();
    move_left = new MoveLeft();
    move_right = new MoveRight();
    jump = new Jump();

    // Player 1
    commands[SDLK_UP]       = move_up;
    commands[SDLK_LEFT]     = move_left;
    commands[SDLK_DOWN]     = move_down;
    commands[SDLK_RIGHT]    = move_right;
    commands[SDLK_SPACE]    = jump;

    // Player 2
    //commands[SDLK_w]        = move_up;
    //commands[SDLK_a]        = move_left;
    //commands[SDLK_s]        = move_down;
    //commands[SDLK_d]        = move_right;
    //commands[SDLK_LSHIFT]   = jump;
}

void InputHandler::configure(int key, Command *command)
{
    commands[key] = command;    // key points to newly assigned command
}

bool InputHandler::fill(std::vector<Command*> &command_queue)
{
    bool exit = input_mapping();    // converts raw input datum to an action and/or state

    if (exit) return true;
    else {
        dispatcher(command_queue);  // fills command queue
        action_map.clear();         // clears key presses
        return false;
    }
}

bool InputHandler::input_mapping()
{
    SDL_Event event;
    while (SDL_PollEvent(&event))
        if (event.type == SDL_QUIT) return true;
        else if (event.type == SDL_KEYDOWN) {
            if (event.key.keysym.sym == SDLK_ESCAPE) return true;
            keydown(event);
        }
        else if (event.type == SDL_KEYUP)
            keyup(event);

    return false;
}

void InputHandler::dispatcher(std::vector<Command*> &command_queue)
{
    std::map<int, Command*>::iterator iter;
    for (iter = commands.begin(); iter != commands.end(); iter++) {
        if (is_held(iter->first) && iter->second->get_input_type() == STATE)
            command_queue.push_back(iter->second);
        else if (was_pressed(iter->first) && iter->second->get_input_type() == ACTION)
            command_queue.push_back(iter->second);
    }
}

void InputHandler::keydown(SDL_Event &event)
{
    if (state_map[event.key.keysym.sym] == RELEASED)
        action_map[event.key.keysym.sym] = EXECUTE;
    state_map[event.key.keysym.sym] = PRESSED;
}

void InputHandler::keyup(SDL_Event &event)
{
    state_map[event.key.keysym.sym] = RELEASED;
}

bool InputHandler::is_held(int key)
{
    return state_map[key];
}

bool InputHandler::was_pressed(int key)
{
    return action_map[key];
}

InputHandler::~InputHandler()
{
    // Delete all command pointers    
    std::map<int, Command*>::iterator iter;
    for (iter = commands.begin(); iter != commands.end(); iter++)
        delete iter->second;
}


input_constants.hpp

#ifndef INPUT_CONSTANTS_HPP
#define INPUT_CONSTANTS_HPP

enum InputType
{
    ACTION,
    STATE,
    RANGE
};

enum Action
{
    EXECUTE = true,
    STOP = false
};

enum State
{
    PRESSED = true,
    RELEASED = false
};

#endif // INPUT_CONSTANTS_HPP


game.hpp(仅相关行)

#include "input_handler.hpp"

class Game
{
    private:
        bool exit;
        InputHandler *input_handler;
        Character *character;
        void update();

    public:
        void execute();
};


game.cpp(仅相关行)

#include "game.hpp"

Game::Game(int screen_width, int screen_height)
{
    exit = false;

    // Initialize input handler
    input_handler = new InputHandler();

    // Create character
    character = new Character("Rouge", 100, 300);

    // Command queue
    std::vector<Command*> command_queue;
}

void Game::execute()
{
    while(!exit) {
        // Handle input
        exit = input_handler->fill(command_queue);
        update ();
    }
}

void Game::update()
{
    // Update character state
    while (!command_queue.empty()) {
        command_queue.back()->execute(character);
        command_queue.pop_back();
    }
}


#1 楼


这是命令模式的正确实现和使用吗?


是的,我认为它足够好。





性能明智的做法是,很难确定其他解决方案是否会更快。
这个解决方案很容易维护且可扩展,但是在这种情况下我有偏见,因为我真的很欣赏Command模式。

如果要摆脱
您现在正在使用的std::map。您可以很好地使用数组,因为您的
映射键只是整数常量。如果要确保枚举常量常量是连续的,则可以替换为:

std::map<int, Command*> commands;
std::map<int, State>    state_map;
std::map<int, Action>   action_map;


用普通数组:

Command* commands[MAX_COMMAND_INDEX];
State    state_map[MAX_STATE_INDEX];
Action   action_map[MAX_ACTION_INDEX];


或者更好,使用C ++ 11数组:

std::array<Command*, MAX_ACTION_INDEX> commands;
std::array<State,    MAX_ACTION_INDEX> state_map;
std::array<Action,   MAX_ACTION_INDEX> action_map;


常规改进:

您可以使用智能指针来实施更安全的对象所有权策略。
C ++ 11 std::shared_ptr将是一个不错的选择:

typedef std::shared_ptr<Command> CommandPtr;


这确保了Command传递时永远不会被破坏,从而免除了您在InputHandler的析构函数中手动删除它们的负担。

实际上,对于大多数(如果不是全部)对象,应该使用智能指针,其中包括
Character指针:

typedef std::shared_ptr<Character> CharacterPtr;


因此,在virtual void execute(CharacterPtr character)Command中,即使执行了character指针,该指针始终有效。原始指针的引用在其他线程中丢失。

更好的命名方式:

公共功能:

void configure(int key, Command *command);

InputHandler的作用是将给定键绑定到命令,因此,
将使用更好的名称进行绑定:

void bind(int key, CommandPtr command);


fill也是一个函数的模糊名称,该函数负责生成游戏框架的
输入命令:

bool fill(std::vector<Command*> &command_queue);


我建议将其重命名为:

bool generate_input(std::vector<CommandPtr> &command_queue);


,或更明确的名称:

bool generate_input_commands(std::vector<CommandPtr> &command_queue);


不要害怕使用长名。您留给歧义的余地越少越好。

bool input_mapping();


这个名字也有些可怜。请使用您在其调用站点上所写的推荐内容
,并尝试使用该功能的描述性名称:

// converts raw input datum to an action and/or state


您可以将其重命名为:

bool convert_inputs_to_actions();

bool map_inputs_to_actions();

bool do_input_to_action_mapping();


列表会继续。

void dispatcher(std::vector<Command*> &command_queue);


命令队列。您选择的名称适合于类型
(例如Dispatcher类),但不适用于函数。
您可以将其重命名为:

void dispatch_commands(std::vector<CommandPtr> &command_queue);


或更简单地说:

void fill_command_queue(std::vector<CommandPtr> &command_queue);


请注意您的控制流布局样式:

可以忽略此内容个人喜好,但我认为值得一提。

我建议您注意将return语句放在if子句同一行的样式。例如,在此代码块中:

while (SDL_PollEvent(&event))
    if (event.type == SDL_QUIT) return true;
    else if (event.type == SDL_KEYDOWN) {
        if (event.key.keysym.sym == SDLK_ESCAPE) return true;
        keydown(event);
    }
    else if (event.type == SDL_KEYUP)
        keyup(event);


我发现样式和花括号的混合很难读。
如果在眼睛上看起来容易得多您应将其重新格式化为:

while (SDL_PollEvent(&event))
    if (event.type == SDL_QUIT)
        return true;
    else if (event.type == SDL_KEYDOWN) {
        if (event.key.keysym.sym == SDLK_ESCAPE)
            return true;
        keydown(event);
    }
    else if (event.type == SDL_KEYUP)
        keyup(event);


,但如果在所有语句上添加统一的支撑,甚至会更好,更安全,甚至在单行语句中也是如此:

while (SDL_PollEvent(&event)) {
    if (event.type == SDL_QUIT) {
        return true;
    } else if (event.type == SDL_KEYDOWN) {
        if (event.key.keysym.sym == SDLK_ESCAPE) {
            return true;
        }
        keydown(event);
    } else if (event.type == SDL_KEYUP) {
        keyup(event);
    }
}


使用统一的支撑,如果您需要在if结果中添加多个命令
,则没有做类似以下愚蠢操作的风险:

if (x == 42)
    do_something();
    do_some_other_thing();


正确的缩进会让您以为这两行都属于第一个if的结果
。如果程序员从一开始就做好了准备,那么他将不会受到此类错误的影响。


如何使其适应来自多个控制器的输入?


您已经将一些特定于游戏的逻辑硬编码到InputHandler的构造函数中。
最直接,最可能的最佳解决方法是子类化。

您可以定义一个或多个必须实现的虚拟方法。由您的代码的客户端:

class InputHandler
{
    // all the previous stuff...

protected:

    // Classes extending InputHandler will define their
    // own mappings inside this method.
    virtual void setup_input_mappings() = 0;
};


,这样,对于每个您希望支持的硬件/平台,您就可以拥有一个InputHandler类型:

class InputHandlerXBOXController : public InputHandler
{
    void setup_input_mappings()
    {
        commands[XB_Y]  = move_up;
        commands[XB_X]  = move_left;
        commands[XB_A]  = move_down;
        commands[XB_B]  = move_right;
        commands[XB_L2] = jump;
    }
};


例如,您可以使用模板方法的同一概念来隔离
InputHandler与SDL的其余部分。

评论


\ $ \ begingroup \ $
对我来说,与CommandPtr CommandPointer ThisIsAPointerToAnObjectOfTypeCommand和自定义名称下定义的其他任何类型相比,Command *更清楚地表明它是指向Command类型的对象的指针。
\ $ \ endgroup \ $
– Nikos
18-09-15在10:53

\ $ \ begingroup \ $
使用C ++ 11和使用typedef?真?
\ $ \ endgroup \ $
–津安
18-10-25在8:11



#2 楼

我认为您没有以正确的方式实现命令模式。

命令模式背后的想法是封装从receiver开始执行receiver方法的逻辑。一个很好的例子是model类。您不需要model更改自己的状态。因此,实现此目的的一种好方法是将model的所有设置方法封装在命令中。

您的命令确实做到了这一点,但是它也是如此:

virtual InputType get_input_type() = 0;


这绝对不属于那里。

您正在使用命令状态来确定client(InputHandler)行为。您应该为此使用模型。

考虑更好的初始化命令的方法,并考虑将command_queue也移至模型中,因此您的clientcontroller只会做逻辑,不会处理应用程序指出,即使它不是business logic