From 5bb7fa8c1386e78b804e6fa3a755981855e82801 Mon Sep 17 00:00:00 2001 From: Doyle Thai Date: Fri, 25 Nov 2016 18:03:25 +1100 Subject: [PATCH] Improve input handling using state flags Fix reading one shot inputs not working due to prematurely clearing out state data tracking input changes between one frame and another. The transition count used to track this now stores difference between each frame in the input flags. This also solves requiring at the end of the frame to update the transition counts at the end of each update loop. --- src/Asteroid.c | 117 ++++------------------------ src/Platform.c | 85 ++++++++++++++++++++ src/UserInterface.c | 43 +++++----- src/dengine.c | 3 +- src/include/Dengine/Asteroid.h | 2 +- src/include/Dengine/Common.h | 7 ++ src/include/Dengine/Platform.h | 38 ++++++++- src/include/Dengine/UserInterface.h | 8 +- 8 files changed, 173 insertions(+), 130 deletions(-) diff --git a/src/Asteroid.c b/src/Asteroid.c index 2757d0e..eb53c61 100644 --- a/src/Asteroid.c +++ b/src/Asteroid.c @@ -147,68 +147,6 @@ void initRenderer(GameState *state, v2 windowSize) } } -enum ReadKeyType -{ - readkeytype_oneShot, - readkeytype_delayedRepeat, - readkeytype_repeat, - readkeytype_count, -}; - -#define KEY_DELAY_NONE 0.0f -INTERNAL b32 getKeyStatus(KeyState *key, enum ReadKeyType readType, - f32 delayInterval, f32 dt) -{ - - // TODO(doyle): Don't let get key status modify keyinput state - if (!key->endedDown) return FALSE; - - switch(readType) - { - case readkeytype_oneShot: - { - if (key->newHalfTransitionCount > key->oldHalfTransitionCount) - return TRUE; - break; - } - case readkeytype_repeat: - case readkeytype_delayedRepeat: - { - if (key->newHalfTransitionCount > key->oldHalfTransitionCount) - { - if (readType == readkeytype_delayedRepeat) - { - // TODO(doyle): Let user set arbitrary delay after initial input - key->delayInterval = 2 * delayInterval; - } - else - { - key->delayInterval = delayInterval; - } - return TRUE; - } - else if (key->delayInterval <= 0.0f) - { - key->delayInterval = delayInterval; - return TRUE; - } - else - { - key->delayInterval -= dt; - } - break; - } - default: -#ifdef DENGINE_DEBUG - DEBUG_LOG("getKeyStatus() error: Invalid ReadKeyType enum"); - ASSERT(INVALID_CODE_PATH); -#endif - break; - } - - return FALSE; -} - #include #include v2 *createAsteroidVertexList(MemoryArena_ *arena, i32 iterations, @@ -697,33 +635,10 @@ void asteroid_gameUpdateAndRender(GameState *state, Memory *memory, for (u32 i = world->asteroidCounter; i < world->numAsteroids; i++) addAsteroid(world, (rand() % asteroidsize_count)); - { - KeyState *keys = state->input.keys; - for (enum KeyCode code = 0; code < keycode_count; code++) - { - KeyState *keyState = &keys[code]; + platform_processInputBuffer(&state->input, dt); - u32 halfTransitionCount = keyState->newHalfTransitionCount - - keyState->oldHalfTransitionCount; - - if (halfTransitionCount > 0) - { - b32 transitionCountIsOdd = ((halfTransitionCount & 1) == 1); - - if (transitionCountIsOdd) - { - if (keyState->endedDown) keyState->endedDown = FALSE; - else keyState->endedDown = TRUE; - } - - keyState->oldHalfTransitionCount = - keyState->newHalfTransitionCount; - } - } - } - - if (getKeyStatus(&state->input.keys[keycode_left_square_bracket], - readkeytype_repeat, 0.2f, dt)) + if (platform_queryKey(&state->input.keys[keycode_left_square_bracket], + readkeytype_repeat, 0.2f)) { addAsteroid(world, (rand() % asteroidsize_count)); } @@ -739,8 +654,8 @@ void asteroid_gameUpdateAndRender(GameState *state, Memory *memory, v2 ddP = {0}; if (entity->type == entitytype_ship) { - if (getKeyStatus(&state->input.keys[keycode_up], readkeytype_repeat, - 0.0f, dt)) + if (platform_queryKey(&state->input.keys[keycode_up], + readkeytype_repeat, 0.0f)) { // TODO(doyle): Renderer creates upfacing triangles by default, // but we need to offset rotation so that our base "0 degrees" @@ -753,20 +668,20 @@ void asteroid_gameUpdateAndRender(GameState *state, Memory *memory, } Degrees rotationsPerSecond = 180.0f; - if (getKeyStatus(&state->input.keys[keycode_left], - readkeytype_repeat, 0.0f, dt)) + if (platform_queryKey(&state->input.keys[keycode_left], + readkeytype_repeat, 0.0f)) { - entity->rotation += (rotationsPerSecond) * dt; + entity->rotation += (rotationsPerSecond)*dt; } - if (getKeyStatus(&state->input.keys[keycode_right], - readkeytype_repeat, 0.0f, dt)) + if (platform_queryKey(&state->input.keys[keycode_right], + readkeytype_repeat, 0.0f)) { - entity->rotation -= (rotationsPerSecond) * dt; + entity->rotation -= (rotationsPerSecond)*dt; } - if (getKeyStatus(&state->input.keys[keycode_space], - readkeytype_delayedRepeat, 0.05f, dt)) + if (platform_queryKey(&state->input.keys[keycode_space], + readkeytype_one_shot, KEY_DELAY_NONE)) { addBullet(world, entity); @@ -882,11 +797,9 @@ void asteroid_gameUpdateAndRender(GameState *state, Memory *memory, continue; } - f32 dPMultiplier = 10; Radians rotation = DEGREES_TO_RADIANS((entity->rotation + 90.0f)); - ddP = V2(math_cosf(rotation), math_sinf(rotation)); - entity->dP = v2_scale(ddP, world->pixelsPerMeter * dPMultiplier); - + v2 localDp = V2(math_cosf(rotation), math_sinf(rotation)); + entity->dP = v2_scale(localDp, world->pixelsPerMeter * 10); } /* Loop entity around world */ diff --git a/src/Platform.c b/src/Platform.c index c874b78..ce85ba2 100644 --- a/src/Platform.c +++ b/src/Platform.c @@ -94,3 +94,88 @@ i32 platform_readFileToBuffer(MemoryArena_ *arena, const char *const filePath, return 0; } + +void platform_processInputBuffer(InputBuffer *inputBuffer, f32 dt) +{ + KeyState *keyBuffer = inputBuffer->keys; + for (enum KeyCode code = 0; code < keycode_count; code++) + { + KeyState *key = &keyBuffer[code]; + + u32 halfTransitionCount = + key->newHalfTransitionCount - key->oldHalfTransitionCount; + + if (halfTransitionCount > 0) + { + b32 transitionCountIsOdd = ((halfTransitionCount & 1) == 1); + if (transitionCountIsOdd) + { + /* If it was not last ended down, then update interval if + * necessary */ + if (!common_isSet(key->flags, keystateflag_ended_down)) + { + if (key->delayInterval > 0) key->delayInterval -= dt; + + key->flags |= keystateflag_pressed_on_curr_frame; + } + key->flags ^= keystateflag_ended_down; + } + } + else + { + key->flags &= (~keystateflag_pressed_on_curr_frame); + } + + key->newHalfTransitionCount = key->oldHalfTransitionCount; + } +} + +b32 platform_queryKey(KeyState *key, enum ReadKeyType readType, + f32 delayInterval) +{ + + if (!common_isSet(key->flags, keystateflag_ended_down)) return FALSE; + + switch (readType) + { + case readkeytype_one_shot: + { + if (common_isSet(key->flags, keystateflag_pressed_on_curr_frame)) + return TRUE; + } + break; + + case readkeytype_repeat: + case readkeytype_delay_repeat: + { + if (common_isSet(key->flags, keystateflag_pressed_on_curr_frame)) + { + if (readType == readkeytype_delay_repeat) + { + // TODO(doyle): Let user set arbitrary delay after initial input + key->delayInterval = 2 * delayInterval; + } + else + { + key->delayInterval = delayInterval; + } + return TRUE; + } + else if (key->delayInterval <= 0.0f) + { + key->delayInterval = delayInterval; + return TRUE; + } + } + break; + + default: + { + ASSERT(INVALID_CODE_PATH); + } + break; + } + + return FALSE; +} + diff --git a/src/UserInterface.c b/src/UserInterface.c index 9c00c1d..7487468 100644 --- a/src/UserInterface.c +++ b/src/UserInterface.c @@ -7,7 +7,7 @@ i32 userInterface_button(UiState *const uiState, MemoryArena_ *const arena, AssetManager *const assetManager, Renderer *const renderer, Font *const font, - const KeyInput input, const i32 id, const Rect rect, + const InputBuffer input, const i32 id, const Rect rect, const char *const label) { if (math_pointInRect(rect, input.mouseP)) @@ -23,7 +23,8 @@ i32 userInterface_button(UiState *const uiState, MemoryArena_ *const arena, uiState->activeItem == uiState->debugWindow.id || uiState->activeItem == 0) { - if (input.keys[keycode_mouseLeft].endedDown) + if (common_isSet(input.keys[keycode_mouseLeft].flags, + keystateflag_ended_down)) { uiState->activeItem = id; } @@ -124,9 +125,9 @@ i32 userInterface_button(UiState *const uiState, MemoryArena_ *const arena, // If button is hot and active, but mouse button is not // down, the user must have clicked the button. - if (!input.keys[keycode_mouseLeft].endedDown && - uiState->hotItem == id && - uiState->activeItem == id) + if (!common_isSet(input.keys[keycode_mouseLeft].flags, + keystateflag_ended_down) && + uiState->hotItem == id && uiState->activeItem == id) { return id; } @@ -136,7 +137,7 @@ i32 userInterface_button(UiState *const uiState, MemoryArena_ *const arena, i32 userInterface_scrollbar(UiState *const uiState, AssetManager *const assetManager, - Renderer *const renderer, const KeyInput input, + Renderer *const renderer, const InputBuffer input, const i32 id, const Rect scrollBarRect, i32 *const value, const i32 maxValue) { @@ -151,7 +152,8 @@ i32 userInterface_scrollbar(UiState *const uiState, uiState->activeItem == uiState->debugWindow.id || uiState->activeItem == 0) { - if (input.keys[keycode_mouseLeft].endedDown) + if (common_isSet(input.keys[keycode_mouseLeft].flags, + keystateflag_ended_down)) { uiState->activeItem = id; } @@ -253,7 +255,7 @@ i32 userInterface_scrollbar(UiState *const uiState, i32 userInterface_textField(UiState *const uiState, MemoryArena_ *const arena, AssetManager *const assetManager, Renderer *const renderer, Font *const font, - KeyInput input, const i32 id, const Rect rect, + InputBuffer input, const i32 id, const Rect rect, char *const string) { i32 strLen = common_strlen(string); @@ -266,7 +268,8 @@ i32 userInterface_textField(UiState *const uiState, MemoryArena_ *const arena, uiState->activeItem == uiState->debugWindow.id || uiState->activeItem == 0) { - if (input.keys[keycode_mouseLeft].endedDown) + if (common_isSet(input.keys[keycode_mouseLeft].flags, + keystateflag_ended_down)) { uiState->activeItem = id; } @@ -338,9 +341,9 @@ i32 userInterface_textField(UiState *const uiState, MemoryArena_ *const arena, } } - if (!input.keys[keycode_mouseLeft].endedDown && - uiState->hotItem == id && - uiState->activeItem == id) + if (!common_isSet(input.keys[keycode_mouseLeft].flags, + keystateflag_ended_down) && + uiState->hotItem == id && uiState->activeItem == id) { uiState->kbdItem = id; } @@ -354,12 +357,14 @@ i32 userInterface_textField(UiState *const uiState, MemoryArena_ *const arena, i32 userInterface_window(UiState *const uiState, MemoryArena_ *const arena, AssetManager *const assetManager, Renderer *const renderer, Font *const font, - const KeyInput input, WindowState *window) + const InputBuffer input, WindowState *window) { if (math_pointInRect(window->rect, input.mouseP)) { uiState->hotItem = window->id; - if (uiState->activeItem == 0 && input.keys[keycode_mouseLeft].endedDown) + if (uiState->activeItem == 0 && + common_isSet(input.keys[keycode_mouseLeft].flags, + keystateflag_ended_down)) uiState->activeItem = window->id; } @@ -425,7 +430,8 @@ i32 userInterface_window(UiState *const uiState, MemoryArena_ *const arena, if (window->windowHeld) { - if (!input.keys[keycode_mouseLeft].endedDown) + if (!common_isSet(input.keys[keycode_mouseLeft].flags, + keystateflag_ended_down)) { window->windowHeld = FALSE; window->prevFrameWindowHeld = FALSE; @@ -458,10 +464,9 @@ i32 userInterface_window(UiState *const uiState, MemoryArena_ *const arena, window->prevMouseP = input.mouseP; } - - if (!input.keys[keycode_mouseLeft].endedDown && - uiState->hotItem == window->id && - uiState->activeItem == window->id) + if (!common_isSet(input.keys[keycode_mouseLeft].flags, + keystateflag_ended_down) && + uiState->hotItem == window->id && uiState->activeItem == window->id) { return window->id; } diff --git a/src/dengine.c b/src/dengine.c index 5ade1fc..016183e 100644 --- a/src/dengine.c +++ b/src/dengine.c @@ -56,7 +56,8 @@ INTERNAL void keyCallback(GLFWwindow *window, int key, int scancode, int action, i32 offset = 0; if (key >= 'A' && key <= 'Z') { - if (!game->input.keys[keycode_leftShift].endedDown) + KeyState *leftShiftKey = &game->input.keys[keycode_leftShift]; + if (!common_isSet(leftShiftKey->flags, keystateflag_ended_down)) offset = 'a' - 'A'; } diff --git a/src/include/Dengine/Asteroid.h b/src/include/Dengine/Asteroid.h index e37b8db..2ec46b4 100644 --- a/src/include/Dengine/Asteroid.h +++ b/src/include/Dengine/Asteroid.h @@ -46,7 +46,7 @@ typedef struct GameState { AudioManager audioManager; AssetManager assetManager; - KeyInput input; + InputBuffer input; Renderer renderer; World world; diff --git a/src/include/Dengine/Common.h b/src/include/Dengine/Common.h index bd525f0..5b5e2bb 100644 --- a/src/include/Dengine/Common.h +++ b/src/include/Dengine/Common.h @@ -44,6 +44,13 @@ u8 *common_memset(u8 *const ptr, const i32 value, const i32 numBytes); void common_itoa(i32 value, char *buf, i32 bufSize); i32 common_atoi(const char *string, const i32 len); +inline b32 common_isSet(u32 bitfield, u32 flags) +{ + b32 result = FALSE; + if ((bitfield & flags) == flags) result = TRUE; + + return result; +} //----------------------------------------------------------------------------- // MurmurHash2, by Austin Appleby diff --git a/src/include/Dengine/Platform.h b/src/include/Dengine/Platform.h index 159cff2..d9c2077 100644 --- a/src/include/Dengine/Platform.h +++ b/src/include/Dengine/Platform.h @@ -119,19 +119,33 @@ enum KeyCode keycode_null, }; +/* + NOTE(doyle): EndedDown describes the last state the key was in and is + _NOT_ the same as pressed on current frame. They key may of have been + held down over multiple frames, so endedDown will remain true. + + Pressed on current frame captures if it was just pressed on this frame + only. +*/ +enum KeyStateFlag +{ + keystateflag_ended_down = (1 << 0), + keystateflag_pressed_on_curr_frame = (1 << 1), +}; + typedef struct KeyState { f32 delayInterval; u32 oldHalfTransitionCount; u32 newHalfTransitionCount; - b32 endedDown; + u32 flags; } KeyState; -typedef struct KeyInput +typedef struct InputBuffer { v2 mouseP; KeyState keys[keycode_count]; -} KeyInput; +} InputBuffer; typedef struct PlatformFileRead { @@ -155,4 +169,22 @@ void platform_closeFileRead(MemoryArena_ *arena, PlatformFileRead *file); i32 platform_readFileToBuffer(MemoryArena_ *arena, const char *const filePath, PlatformFileRead *file); +/* + NOTE(doyle): The keyinput functions are technically not for "communicating to + the platform layer", but I've decided to group it here alongside the input + data definitions. Since we require that the platform layer writes directly + into our input buffer located in the game state. + */ +#define KEY_DELAY_NONE 0.0f + +enum ReadKeyType +{ + readkeytype_one_shot, + readkeytype_delay_repeat, + readkeytype_repeat, + readkeytype_count, +}; +void platform_processInputBuffer(InputBuffer *inputBuffer, f32 dt); +b32 platform_queryKey(KeyState *key, enum ReadKeyType readType, + f32 delayInterval); #endif diff --git a/src/include/Dengine/UserInterface.h b/src/include/Dengine/UserInterface.h index e469514..6bf7d0e 100644 --- a/src/include/Dengine/UserInterface.h +++ b/src/include/Dengine/UserInterface.h @@ -79,23 +79,23 @@ inline i32 userInterface_generateId(UiState *const uiState) i32 userInterface_button(UiState *const uiState, MemoryArena_ *const arena, AssetManager *const assetManager, Renderer *const renderer, Font *const font, - const KeyInput input, const i32 id, const Rect rect, + const InputBuffer input, const i32 id, const Rect rect, const char *const label); i32 userInterface_textField(UiState *const uiState, MemoryArena_ *const arena, AssetManager *const assetManager, Renderer *const renderer, Font *const font, - KeyInput input, const i32 id, const Rect rect, + InputBuffer input, const i32 id, const Rect rect, char *const string); i32 userInterface_scrollbar(UiState *const uiState, AssetManager *const assetManager, - Renderer *const renderer, const KeyInput input, + Renderer *const renderer, const InputBuffer input, const i32 id, const Rect scrollBarRect, i32 *const value, const i32 maxValue); i32 userInterface_window(UiState *const uiState, MemoryArena_ *const arena, AssetManager *const assetManager, Renderer *const renderer, Font *const font, - const KeyInput input, WindowState *window); + const InputBuffer input, WindowState *window); #endif