Browse Source

a simple implement for game controller

kdmcser 1 year ago
parent
commit
beaebb3a5f

+ 4 - 0
client/CMakeLists.txt

@@ -34,6 +34,8 @@ set(client_SRCS
 	eventsSDL/InputSourceMouse.cpp
 	eventsSDL/InputSourceText.cpp
 	eventsSDL/InputSourceTouch.cpp
+	eventsSDL/InputSourceGameController.cpp
+	eventsSDL/GameControllerShortcuts.cpp
 
 	gui/CGuiHandler.cpp
 	gui/CIntObject.cpp
@@ -207,6 +209,8 @@ set(client_HEADERS
 	eventsSDL/InputSourceMouse.h
 	eventsSDL/InputSourceText.h
 	eventsSDL/InputSourceTouch.h
+    eventsSDL/InputSourceGameController.h
+	eventsSDL/GameControllerShortcuts.h
 
 	gui/CGuiHandler.h
 	gui/CIntObject.h

+ 69 - 0
client/eventsSDL/GameControllerShortcuts.cpp

@@ -0,0 +1,69 @@
+/*
+* GameControllerShortcuts.cpp, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+
+#include "StdInc.h"
+#include "GameControllerShortcuts.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/ShortcutHandler.h"
+
+
+std::vector<EShortcut> getButtonBShortcuts()
+{
+    auto shortcuts =  GH.shortcuts().translateKeycode(SDLK_ESCAPE);
+    // avoid someday ADVENTURE_EXIT_WORLD_VIEW be add in SDLK_ESCAPE shortcuts
+    if(std::find(shortcuts.begin(), shortcuts.end(), EShortcut::ADVENTURE_EXIT_WORLD_VIEW) == shortcuts.end())
+        shortcuts.push_back(EShortcut::ADVENTURE_EXIT_WORLD_VIEW);
+    return shortcuts;
+}
+
+std::vector<EShortcut> getButtonXShortcuts()
+{
+    auto shortcuts =  GH.shortcuts().translateKeycode(SDLK_RETURN);
+    // avoid someday ADVENTURE_EXIT_WORLD_VIEW be add in SDLK_RETURN shortcuts
+    if(std::find(shortcuts.begin(), shortcuts.end(), EShortcut::ADVENTURE_EXIT_WORLD_VIEW) == shortcuts.end())
+        shortcuts.push_back(EShortcut::ADVENTURE_EXIT_WORLD_VIEW);
+    return shortcuts;
+}
+
+const ButtonShortcutsMap & getButtonShortcutsMap() {
+    static const ButtonShortcutsMap buttonShortcutsMap =
+    {
+        // SDL_CONTROLLER_BUTTON_A for mouse left click
+        {SDL_CONTROLLER_BUTTON_B, getButtonBShortcuts()},
+        {SDL_CONTROLLER_BUTTON_X, getButtonXShortcuts()},
+        // SDL_CONTROLLER_BUTTON_Y for mouse right click
+        {SDL_CONTROLLER_BUTTON_LEFTSHOULDER, {EShortcut::ADVENTURE_NEXT_HERO, EShortcut::BATTLE_DEFEND}},
+        {SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, {EShortcut::ADVENTURE_NEXT_TOWN, EShortcut::BATTLE_WAIT}},
+        {SDL_CONTROLLER_BUTTON_BACK, {EShortcut::GAME_END_TURN, EShortcut::BATTLE_END_WITH_AUTOCOMBAT}},
+        {SDL_CONTROLLER_BUTTON_START, {EShortcut::GLOBAL_OPTIONS, EShortcut::ADVENTURE_GAME_OPTIONS}},
+        {SDL_CONTROLLER_BUTTON_DPAD_UP, {EShortcut::MOVE_UP, EShortcut::ADVENTURE_VIEW_WORLD,
+                                         EShortcut::RECRUITMENT_UPGRADE,
+                                         EShortcut::RECRUITMENT_UPGRADE_ALL,
+                                         EShortcut::BATTLE_CONSOLE_UP, EShortcut::RECRUITMENT_MAX}},
+        {SDL_CONTROLLER_BUTTON_DPAD_DOWN, {EShortcut::MOVE_DOWN, EShortcut::ADVENTURE_KINGDOM_OVERVIEW,
+                                           EShortcut::BATTLE_CONSOLE_DOWN, EShortcut::RECRUITMENT_MIN}},
+        {SDL_CONTROLLER_BUTTON_DPAD_LEFT, {EShortcut::MOVE_LEFT, EShortcut::ADVENTURE_VIEW_SCENARIO}},
+        {SDL_CONTROLLER_BUTTON_DPAD_RIGHT, {EShortcut::MOVE_RIGHT, EShortcut::ADVENTURE_THIEVES_GUILD}},
+        {SDL_CONTROLLER_BUTTON_LEFTSTICK, {EShortcut::ADVENTURE_TOGGLE_MAP_LEVEL,
+                                           EShortcut::BATTLE_TOGGLE_HEROES_STATS}},
+        {SDL_CONTROLLER_BUTTON_RIGHTSTICK, {EShortcut::ADVENTURE_TOGGLE_GRID, EShortcut::BATTLE_TOGGLE_QUEUE}}
+    };
+    return buttonShortcutsMap;
+}
+
+const TriggerShortcutsMap & getTriggerShortcutsMap()
+{
+    static const TriggerShortcutsMap triggerShortcutsMap = {
+        {SDL_CONTROLLER_AXIS_TRIGGERLEFT, {EShortcut::ADVENTURE_VISIT_OBJECT, EShortcut::BATTLE_TACTICS_NEXT,
+                                           EShortcut::BATTLE_USE_CREATURE_SPELL}},
+        {SDL_CONTROLLER_AXIS_TRIGGERRIGHT, {EShortcut::ADVENTURE_CAST_SPELL, EShortcut::BATTLE_CAST_SPELL}}
+    };
+    return triggerShortcutsMap;
+}

+ 21 - 0
client/eventsSDL/GameControllerShortcuts.h

@@ -0,0 +1,21 @@
+/*
+* GameControllerShortcuts.h, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+
+#pragma once
+
+#include <map>
+#include <SDL.h>
+#include "../gui/Shortcut.h"
+
+using ButtonShortcutsMap = std::map<int, std::vector<EShortcut> >;
+using TriggerShortcutsMap = std::map<int, std::vector<EShortcut> >;
+
+const ButtonShortcutsMap & getButtonShortcutsMap();
+const TriggerShortcutsMap & getTriggerShortcutsMap();

+ 30 - 0
client/eventsSDL/InputHandler.cpp

@@ -16,6 +16,7 @@
 #include "InputSourceKeyboard.h"
 #include "InputSourceTouch.h"
 #include "InputSourceText.h"
+#include "InputSourceGameController.h"
 
 #include "../gui/CGuiHandler.h"
 #include "../gui/CursorHandler.h"
@@ -36,6 +37,7 @@ InputHandler::InputHandler()
 	, keyboardHandler(std::make_unique<InputSourceKeyboard>())
 	, fingerHandler(std::make_unique<InputSourceTouch>())
 	, textHandler(std::make_unique<InputSourceText>())
+    , gameControllerHandler(std::make_unique<InputSourceGameController>())
 {
 }
 
@@ -69,6 +71,12 @@ void InputHandler::handleCurrentEvent(const SDL_Event & current)
 			return fingerHandler->handleEventFingerDown(current.tfinger);
 		case SDL_FINGERUP:
 			return fingerHandler->handleEventFingerUp(current.tfinger);
+        case SDL_CONTROLLERAXISMOTION:
+            return gameControllerHandler->handleEventAxisMotion(current.caxis);
+        case SDL_CONTROLLERBUTTONDOWN:
+            return gameControllerHandler->handleEventButtonDown(current.cbutton);
+        case SDL_CONTROLLERBUTTONUP:
+            return gameControllerHandler->handleEventButtonUp(current.cbutton);
 	}
 }
 
@@ -88,6 +96,7 @@ void InputHandler::processEvents()
 	for(const auto & currentEvent : eventsToProcess)
 		handleCurrentEvent(currentEvent);
 
+    gameControllerHandler->handleUpdate();
 	fingerHandler->handleUpdate();
 }
 
@@ -103,6 +112,7 @@ bool InputHandler::ignoreEventsUntilInput()
 			case SDL_MOUSEBUTTONDOWN:
 			case SDL_FINGERDOWN:
 			case SDL_KEYDOWN:
+            case SDL_CONTROLLERBUTTONDOWN:
 				inputFound = true;
 		}
 	}
@@ -196,6 +206,21 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
 			NotificationHandler::handleSdlEvent(ev);
 		}
 	}
+    else if(ev.type == SDL_CONTROLLERDEVICEADDED)
+    {
+        gameControllerHandler->handleEventDeviceAdded(ev.cdevice);
+        return;
+    }
+    else if(ev.type == SDL_CONTROLLERDEVICEREMOVED)
+    {
+        gameControllerHandler->handleEventDeviceRemoved(ev.cdevice);
+        return;
+    }
+    else if(ev.type == SDL_CONTROLLERDEVICEREMAPPED)
+    {
+        gameControllerHandler->handleEventDeviceRemapped(ev.cdevice);
+        return;
+    }
 
 	//preprocessing
 	if(ev.type == SDL_MOUSEMOTION)
@@ -324,3 +349,8 @@ const Point & InputHandler::getCursorPosition() const
 {
 	return cursorPosition;
 }
+
+void InputHandler::tryOpenGameController()
+{
+    gameControllerHandler->tryOpenAllGameControllers();
+}

+ 5 - 0
client/eventsSDL/InputHandler.h

@@ -21,6 +21,7 @@ class InputSourceMouse;
 class InputSourceKeyboard;
 class InputSourceTouch;
 class InputSourceText;
+class InputSourceGameController;
 
 class InputHandler
 {
@@ -39,6 +40,7 @@ class InputHandler
 	std::unique_ptr<InputSourceKeyboard> keyboardHandler;
 	std::unique_ptr<InputSourceTouch> fingerHandler;
 	std::unique_ptr<InputSourceText> textHandler;
+    std::unique_ptr<InputSourceGameController> gameControllerHandler;
 
 public:
 	InputHandler();
@@ -84,4 +86,7 @@ public:
 	bool isKeyboardAltDown() const;
 	bool isKeyboardCtrlDown() const;
 	bool isKeyboardShiftDown() const;
+
+    /// If any game controller available, use it.
+    void tryOpenGameController();
 };

+ 248 - 0
client/eventsSDL/InputSourceGameController.cpp

@@ -0,0 +1,248 @@
+/*
+* InputSourceGameController.cpp, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+
+#include "StdInc.h"
+#include "InputSourceGameController.h"
+#include "GameControllerShortcuts.h"
+#include "InputHandler.h"
+
+#include "../CGameInfo.h"
+#include "../gui/CursorHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/EventDispatcher.h"
+#include "../gui/ShortcutHandler.h"
+
+
+
+void InputSourceGameController::gameControllerDeleter(SDL_GameController * gameController)
+{
+    if(gameController)
+        SDL_GameControllerClose(gameController);
+}
+
+InputSourceGameController::InputSourceGameController():
+    lastCheckTime(0),
+    axisValueX(0),
+    axisValueY(0),
+    planDisX(0.0),
+    planDisY(0.0)
+{
+    // SDL_init has not been called. so it is unnecessary to open joystick.
+}
+
+void InputSourceGameController::tryOpenAllGameControllers()
+{
+    for(int i = 0; i < SDL_NumJoysticks(); ++i)
+        if(SDL_IsGameController(i))
+            openGameController(i);
+        else
+            logGlobal->warn("Joystick %d is an unsupported game controller!", i);
+}
+
+
+void InputSourceGameController::openGameController(int index)
+{
+    SDL_GameController * controller = SDL_GameControllerOpen(index);
+    if(!controller)
+    {
+        logGlobal->error("Fail to open game controller %d!", index);
+        return;
+    }
+    GameControllerPtr controllerPtr(controller, gameControllerDeleter);
+
+    // Need to save joystick index for event. Joystick index may not be equal to index sometimes.
+    int joystickIndex = getJoystickIndex(controllerPtr.get());
+    if(joystickIndex < 0)
+    {
+        logGlobal->error("Fail to get joystick index of game controller %d!", index);
+        return;
+    }
+
+    if(gameControllerMap.find(joystickIndex) != gameControllerMap.end())
+    {
+        logGlobal->warn("Game controller with joystick index %d is already opened.", joystickIndex);
+        return;
+    }
+
+    gameControllerMap.emplace(joystickIndex, std::move(controllerPtr));
+}
+
+int InputSourceGameController::getJoystickIndex(SDL_GameController * controller)
+{
+    SDL_Joystick* joystick = SDL_GameControllerGetJoystick(controller);
+    if(!joystick)
+        return -1;
+
+    SDL_JoystickID instanceID = SDL_JoystickInstanceID(joystick);
+    if(instanceID < 0)
+        return -1;
+    return (int)instanceID;
+}
+
+void InputSourceGameController::handleEventDeviceAdded(const SDL_ControllerDeviceEvent & device)
+{
+    if(gameControllerMap.find(device.which) != gameControllerMap.end())
+    {
+        logGlobal->warn("Game controller %d is already opened.", device.which);
+        return;
+    }
+    openGameController(device.which);
+}
+
+void InputSourceGameController::handleEventDeviceRemoved(const SDL_ControllerDeviceEvent & device)
+{
+    if(gameControllerMap.find(device.which) == gameControllerMap.end())
+    {
+        logGlobal->warn("Game controller %d is not opened before.", device.which);
+        return;
+    }
+    gameControllerMap.erase(device.which);
+}
+
+void InputSourceGameController::handleEventDeviceRemapped(const SDL_ControllerDeviceEvent & device)
+{
+    if(gameControllerMap.find(device.which) == gameControllerMap.end())
+    {
+        logGlobal->warn("Game controller %d is not opened.", device.which);
+        return;
+    }
+    gameControllerMap.erase(device.which);
+    openGameController(device.which);
+}
+
+int InputSourceGameController::getRealAxisValue(int value)
+{
+    if(value < AXIS_DEAD_ZOOM && value > -AXIS_DEAD_ZOOM)
+        return 0;
+    if(value > AXIS_MAX_ZOOM)
+        return AXIS_MAX_ZOOM;
+    if(value < -AXIS_MAX_ZOOM)
+        return -AXIS_MAX_ZOOM;
+    int base = value > 0 ? AXIS_DEAD_ZOOM: -AXIS_DEAD_ZOOM;
+    return (value - base) * AXIS_MAX_ZOOM / (AXIS_MAX_ZOOM - AXIS_DEAD_ZOOM);
+}
+
+void InputSourceGameController::dispatchTriggerShortcuts(const std::vector<EShortcut> & shortcutsVector, int axisValue)
+{
+    if(axisValue >= TRIGGER_PRESS_THRESHOLD)
+        GH.events().dispatchShortcutPressed(shortcutsVector);
+    else
+        GH.events().dispatchShortcutReleased(shortcutsVector);
+}
+
+void InputSourceGameController::handleEventAxisMotion(const SDL_ControllerAxisEvent & axis)
+{
+    const auto & triggerShortcutsMap = getTriggerShortcutsMap();
+    if(axis.axis == SDL_CONTROLLER_AXIS_LEFTX)
+    {
+        axisValueX = getRealAxisValue(axis.value);
+    }
+    else if(axis.axis == SDL_CONTROLLER_AXIS_LEFTY)
+    {
+        axisValueY = getRealAxisValue(axis.value);
+    }
+    else if(triggerShortcutsMap.find(axis.axis) != triggerShortcutsMap.end())
+    {
+        const auto & shortcutsVector = triggerShortcutsMap.find(axis.axis)->second;
+        dispatchTriggerShortcuts(shortcutsVector, axis.value);
+    }
+}
+
+void InputSourceGameController::handleEventButtonDown(const SDL_ControllerButtonEvent & button)
+{
+    const Point & position = GH.input().getCursorPosition();
+    const auto & buttonShortcutsMap = getButtonShortcutsMap();
+
+    // TODO: define keys by user
+    if(button.button == SDL_CONTROLLER_BUTTON_A)
+    {
+        GH.events().dispatchMouseLeftButtonPressed(position, 0);
+    }
+    else if(button.button == SDL_CONTROLLER_BUTTON_Y)
+    {
+        GH.events().dispatchShowPopup(position, 0);
+    }
+    else if(buttonShortcutsMap.find(button.button) != buttonShortcutsMap.end())
+    {
+        const auto & shortcutsVector = buttonShortcutsMap.find(button.button)->second;
+        GH.events().dispatchShortcutPressed(shortcutsVector);
+    }
+}
+
+void InputSourceGameController::handleEventButtonUp(const SDL_ControllerButtonEvent & button)
+{
+    const Point & position = GH.input().getCursorPosition();
+    const auto & buttonShortcutsMap = getButtonShortcutsMap();
+    if(button.button == SDL_CONTROLLER_BUTTON_A)
+    {
+        GH.events().dispatchMouseLeftButtonReleased(position, 0);
+    }
+    else if(button.button == SDL_CONTROLLER_BUTTON_Y)
+    {
+        GH.events().dispatchClosePopup(position);
+    }
+    else if(buttonShortcutsMap.find(button.button) != buttonShortcutsMap.end())
+    {
+        const auto & shortcutsVector = buttonShortcutsMap.find(button.button)->second;
+        GH.events().dispatchShortcutReleased(shortcutsVector);
+    }
+}
+
+void InputSourceGameController::doCursorMove(int deltaX, int deltaY)
+{
+    if(deltaX == 0 && deltaY == 0)
+        return;
+    const Point & screenSize = GH.screenDimensions();
+    const Point &cursorPosition = GH.getCursorPosition();
+    int newX = std::min(std::max(cursorPosition.x + deltaX, 0), screenSize.x);
+    int newY = std::min(std::max(cursorPosition.y + deltaY, 0), screenSize.y);
+    Point targetPosition{newX, newY};
+    GH.input().setCursorPosition(targetPosition);
+    if(CCS && CCS->curh)
+        CCS->curh->cursorMove(GH.getCursorPosition().x, GH.getCursorPosition().y);
+}
+
+int InputSourceGameController::getMoveDis(float planDis)
+{
+    if(planDis >= 0)
+        return std::floor(planDis);
+    else
+        return std::ceil(planDis);
+}
+
+void InputSourceGameController::handleUpdate()
+{
+    auto now = std::chrono::high_resolution_clock::now();
+    auto nowMs = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
+    if(lastCheckTime == 0)
+    {
+        lastCheckTime = nowMs;
+        return;
+    }
+
+    long long deltaTime = nowMs - lastCheckTime;
+
+    if(axisValueX == 0)
+        planDisX = 0;
+    else
+        planDisX += ((float)deltaTime / 1000) * ((float)axisValueX / AXIS_MAX_ZOOM) * AXIS_MOVE_SPEED;
+
+    if(axisValueY == 0)
+        planDisY = 0;
+    else
+        planDisY += ((float)deltaTime / 1000) * ((float)axisValueY / AXIS_MAX_ZOOM) * AXIS_MOVE_SPEED;
+
+    int moveDisX = getMoveDis(planDisX);
+    int moveDisY = getMoveDis(planDisY);
+    planDisX -= moveDisX;
+    planDisY -= moveDisY;
+    doCursorMove(moveDisX, moveDisY);
+    lastCheckTime = nowMs;
+}

+ 57 - 0
client/eventsSDL/InputSourceGameController.h

@@ -0,0 +1,57 @@
+/*
+* InputSourceGameController.h, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+
+#pragma once
+
+#include <memory>
+#include <SDL.h>
+
+#include "../../lib/Point.h"
+#include "../gui/Shortcut.h"
+
+
+const int AXIS_DEAD_ZOOM = 6000;
+const int AXIS_MAX_ZOOM = 32000;
+const int AXIS_MOVE_SPEED = 500;
+const int AXIS_CURSOR_MOVE_INTERVAL = 1000;
+const int TRIGGER_PRESS_THRESHOLD = 8000;
+
+
+/// Class that handles game controller input from SDL events
+class InputSourceGameController
+{
+    static void gameControllerDeleter(SDL_GameController * gameController);
+    using GameControllerPtr = std::unique_ptr<SDL_GameController, decltype(&gameControllerDeleter)>;
+
+    std::map<int, GameControllerPtr> gameControllerMap;
+    long long lastCheckTime;
+    int axisValueX;
+    int axisValueY;
+    float planDisX;
+    float planDisY;
+
+    void openGameController(int index);
+    int getJoystickIndex(SDL_GameController * controller);
+    int getRealAxisValue(int value);
+    void dispatchTriggerShortcuts(const std::vector<EShortcut> & shortcutsVector, int axisValue);
+    void doCursorMove(int deltaX, int deltaY);
+    int getMoveDis(float planDis);
+
+public:
+    InputSourceGameController();
+    void tryOpenAllGameControllers();
+    void handleEventDeviceAdded(const SDL_ControllerDeviceEvent & device);
+    void handleEventDeviceRemoved(const SDL_ControllerDeviceEvent & device);
+    void handleEventDeviceRemapped(const SDL_ControllerDeviceEvent & device);
+    void handleEventAxisMotion(const SDL_ControllerAxisEvent & axis);
+    void handleEventButtonDown(const SDL_ControllerButtonEvent & button);
+    void handleEventButtonUp(const SDL_ControllerButtonEvent & button);
+    void handleUpdate();
+};

+ 3 - 0
client/gui/CGuiHandler.cpp

@@ -79,6 +79,9 @@ void CGuiHandler::init()
 	renderHandlerInstance = std::make_unique<RenderHandler>();
 	shortcutsHandlerInstance = std::make_unique<ShortcutHandler>();
 	framerateManagerInstance = std::make_unique<FramerateManager>(settings["video"]["targetfps"].Integer());
+
+    // This must be called after SDL_init(), so put after screenHandlerInstance init.
+    inputHandlerInstance->tryOpenGameController();
 }
 
 void CGuiHandler::handleEvents()

+ 1 - 1
client/renderSDL/ScreenHandler.cpp

@@ -173,7 +173,7 @@ ScreenHandler::ScreenHandler()
 	SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitor");
 #endif
 
-	if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO))
+	if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER))
 	{
 		logGlobal->error("Something was wrong: %s", SDL_GetError());
 		exit(-1);