Search code examples
c++eventsallegroallegro5

How to press multiple keys at the same time using events in real-time? (Allegro 5)


This is a problem that haunts me for years.

Here's my game.h and game.cpp files:

game.h

#ifndef GAME_H_INCLUDED
#define GAME_H_INCLUDED

#include "init.h"

ALLEGRO_BITMAP *load_bmp(path *s);

struct Actor {
    const char *path;
    ALLEGRO_BITMAP *bmp;
    int x;
    int y;
    int speed;
};

void init_game_bitmaps();
void draw_game_bitmaps();

extern map<string, bool> key_states;
void init_key_states();
void check_states();

void control_actor(Actor *target, int speed);

extern Actor player;

#endif // GAME_H_INCLUDED

game.cpp

#include "game.h"

ALLEGRO_BITMAP *load_bmp(path *s) {
    ALLEGRO_BITMAP *bmp = nullptr;
    bmp = al_load_bitmap(s);
    if (!bmp) {

        al_show_native_message_box(display,
            "Fatal Error!",
            "Failed to load: " ,
            s,
            NULL,
            ALLEGRO_MESSAGEBOX_ERROR);

        al_destroy_display(display);
        return nullptr;

    }

    return bmp;
}

map<string, bool> key_states;
void init_key_states() {

    key_states["UP"] = false;
    key_states["DOWN"] = false;
    key_states["LEFT"] = false;
    key_states["RIGHT"] = false;

}

void check_states() {
    auto key = e.keyboard.keycode;
    for (auto it = key_states.begin(); it != key_states.end(); ++it) {
        if (e.type == ALLEGRO_EVENT_KEY_DOWN) {
            if (key == ALLEGRO_KEY_UP) {
                if (it->first == "UP") {
                    it->second = true;
                }
            }
            if (key == ALLEGRO_KEY_DOWN) {
                if (it->first == "DOWN") {
                    it->second = true;
                }
            }
            if (key == ALLEGRO_KEY_LEFT) {
                if (it->first == "LEFT") {
                    it->second = true;
                }
            }
            if (key == ALLEGRO_KEY_RIGHT) {
                if (it->first == "RIGHT") {
                    it->second = true;
                }
            }
        } else if (e.type == ALLEGRO_EVENT_KEY_UP) {
            if (key == ALLEGRO_KEY_UP) {
                if (it->first == "UP") {
                    it->second = false;
                }
            }
            if (key == ALLEGRO_KEY_DOWN) {
                if (it->first == "DOWN") {
                    it->second = false;
                }
            }
            if (key == ALLEGRO_KEY_LEFT) {
                if (it->first == "LEFT") {
                    it->second = false;
                }
            }
            if (key == ALLEGRO_KEY_RIGHT) {
                if (it->first == "RIGHT") {
                    it->second = false;
                }
            }
        }
        cout << it->first << " : " << it->second << endl;
    }
}

void control_actor(Actor *target, int speed) {
    if (key_states["UP"]) {
        target->y -= speed;
    }
    if (key_states["DOWN"]) {
        target->y += speed;
    }
    if (key_states["LEFT"]) {
        target->x -= speed;
    }
    if (key_states["RIGHT"]) {
        target->x += speed;
    }
}

Actor player = {
    "GFX\\player_up.png",
    nullptr,
    (SCREEN_WIDTH / 2) - (ACTOR_SIZE / 2),
    (SCREEN_HEIGHT / 2) - (ACTOR_SIZE / 2),
    8};

void init_game_bitmaps() {
   player.bmp = load_bmp(player.path);
}

void draw_game_bitmaps() {
    al_draw_bitmap(player.bmp, player.x, player.y, 0);
    al_flip_display();
}

Now here's my main file:

main.cpp

#include "init.h"
#include "game.h"

int main(int argc, char **argv){

    init_all();
    register_all();
    init_game_bitmaps();
    init_key_states();

    while (running) {

        draw_game_bitmaps();
        al_wait_for_event(event_queue, &e);

        if (e.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
            running = false;
        }

        check_states();
        control_actor(&player, player.speed);

        if (e.type == ALLEGRO_EVENT_KEY_DOWN) {
            if (e.keyboard.keycode == ALLEGRO_KEY_ESCAPE) {
                running = false;
            }
            if (e.keyboard.keycode == ALLEGRO_KEY_ENTER) {
                cout << "It works!";
            }
        }

    }

    destroy_all();

    return 0;
}

As you can see, I have a std::map that stores key states (One for each arrow of the keyboard), and then I have a procedure called check_states(), that iterate over all the states at each main loop, and set them to true if their respective arrows are pressed (down), and to false when they are released.

The problem:

If I press UP and keep it holding, and then I press LEFT (Without releasing the UP key), the player will move diagonally. Nevertheless, if I release the LEFT, while still holding the UP key, the player will stop, and the state for UP will be true (And I see this because I'm couting it).

Theory

The al_wait_for_event() waits until the event queue specified is non-empty (Which means that when I press the UP key, it copies the event to e, and then when I press the LEFT, it must cancel UP and assign a new event to e, thus creating the unpleasant LAG when I press more than one key at once). Having that in mind, I've concluded: Well, I could have at least FIVE separate event_queues, and FIVE different "event objects". I've managed to create a vector of event_queues and of events and to iterate over both of them at each main loop while passing each event to its respective event_queue AND THE PROBLEM PERSISTED.

SO, how can I press more than one key in real-time? By real-time I mean real real-time, without lags, without any key canceling each other. The answer is key states? Why? How can I do it using events? Is it possible at all?.

EDIT:

I've changed my control_actor() procedure in order to use al_key_down() instead of checking for events, here's its code:

void control_actor(ALLEGRO_KEYBOARD_STATE *key, Actor *target, int speed) {
    if (al_key_down(key, ALLEGRO_KEY_UP)) {
        target->y -= speed;
        cout << "UP" << endl;
    }
    if (al_key_down(key, ALLEGRO_KEY_DOWN)) {
        target->y += speed;
        cout << "DOWN" << endl;
    }
    if (al_key_down(key, ALLEGRO_KEY_LEFT)) {
        target->x -= speed;
        cout << "LEFT" << endl;
    }
    if (al_key_down(key, ALLEGRO_KEY_RIGHT)) {
        target->x += speed;
        cout << "RIGHT" << endl;
    }
}

And the new main.cpp:

#include "init.h"
#include "game.h"

int main(int argc, char **argv){

    init_all();
    register_all();
    init_game_bitmaps();
    ALLEGRO_KEYBOARD_STATE key;

    while (running) {

        draw_game_bitmaps();
        al_wait_for_event(event_queue, &e);

        al_get_keyboard_state(&key);

        control_actor(&key, &player, player.speed);

        if (e.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
            running = false;
        }

        if (e.type == ALLEGRO_EVENT_KEY_DOWN) {
            if (e.keyboard.keycode == ALLEGRO_KEY_ESCAPE) {
                running = false;
            }
            if (e.keyboard.keycode == ALLEGRO_KEY_ENTER) {
                cout << "It works!";
            }
        }

    }

    destroy_all();

    return 0;
}

The post on the allegro forums linked in the comment is from 2002, and that code does not work anymore on Allegro 5. So I've checked the docs, and I'll tell you: THE PROBLEM PERSISTED. The EXACT same thing happens. One arrow cancels the other and the player stops moving for a while, as soon as I press another arrow at the same time.


Solution

  • The Basic Keyboard Example on the allegro wiki may be of more help than that old post.

    There is no need to manage multiple event queues here. Every key press and release should get pushed into the queue -- you just need to make sure you process every event in the queue.

    Basically, you want a main loop that:

    1. Processes every event currently in the queue
    2. Updates the game state
    3. Redraws the screen

    Here is an example I drafted up:

    #include <stdio.h>
    #include <allegro5/allegro.h>
    #include <allegro5/allegro_primitives.h>
    
    const int SPEED = 5;
    const float FPS = 60;
    
    int main(int argc, char **argv) {
      ALLEGRO_DISPLAY *display = NULL;
      ALLEGRO_EVENT_QUEUE *event_queue = NULL;
      ALLEGRO_TIMER *timer = NULL;
      int x = 0, y = 0;   // position
      int vx = 0, vy = 0; // velocity
    
      // initialize everything we need -- error checking omitted for brevity
      al_init();
      al_install_keyboard();
      al_init_primitives_addon();
      display = al_create_display(640, 480);
      event_queue = al_create_event_queue();
      timer = al_create_timer(1.0 / FPS);
      al_register_event_source(event_queue, al_get_keyboard_event_source());
      al_register_event_source(event_queue, al_get_timer_event_source(timer));
      al_start_timer(timer);
    
      bool done = false;
      while(!done) {
        bool redraw = false;
    
        // process events until queue is empty
        while(!al_is_event_queue_empty(event_queue)) {
          ALLEGRO_EVENT ev;
          al_wait_for_event(event_queue, &ev);
    
          switch(ev.type) {
            case ALLEGRO_EVENT_KEY_DOWN:
              switch(ev.keyboard.keycode) {
                case ALLEGRO_KEY_W:
                  vy -= SPEED; // add upward velocity
                  break;
                case ALLEGRO_KEY_S:
                  vy += SPEED; // add downward velocity
                  break;
                case ALLEGRO_KEY_A:
                  vx -= SPEED; // add leftward velocity
                  break;
                case ALLEGRO_KEY_D:
                  vx += SPEED; // add leftward velocity
                  break;
                case ALLEGRO_KEY_ESCAPE:
                  done = true;
                  break;
              }
              break;
            case ALLEGRO_EVENT_KEY_UP:
              switch(ev.keyboard.keycode) {
                case ALLEGRO_KEY_W:
                  vy += SPEED; // remove upward velocity
                  break;
                case ALLEGRO_KEY_S:
                  vy -= SPEED; // remove downward velocity
                  break;
                case ALLEGRO_KEY_A:
                  vx += SPEED; // remove leftward velocity
                  break;
                case ALLEGRO_KEY_D:
                  vx -= SPEED; // remove leftward velocity
                  break;
              }
              break;
            case ALLEGRO_EVENT_TIMER:
              redraw = true; // time for next frame
              break;
          }
        }
    
        // got through all the events this loop -- redraw if necessary
        if (redraw) {
          // move circle
          x += vx;
          y += vy;
    
          // draw circle
          al_clear_to_color(al_map_rgb(0, 0, 0));
          al_draw_filled_circle(x, y, 20, al_map_rgb(0, 0, 255));
          al_flip_display();
        }
      }
    
      al_destroy_display(display);
    
      return 0;
    }
    

    Note the while(!al_is_event_queue_empty(event_queue)). This ensures that we don't miss any event before moving on to the update loop.

    If you try running the example, the circle should respond appropriately to any combination of the WASD keys. If you hold S+D, it will move diagonally right and down. Release S, and it will continue moving right, but not down.

    Hope this helps!