Jump to content

reepblue

Developers
  • Posts

    2,495
  • Joined

  • Last visited

Blog Entries posted by reepblue

  1. reepblue

    Code
    Level transitions are the old school way of getting your player from one map to another. Here I implemented a system much like HL2. Not much else to say, but I should be ready to talk more soon. (Hopefully!) 
     
  2. reepblue

    Code
    Cyclone was shaping up visually, but I was getting feedback about the sound implementation not being so good. I previously created a script system that defined all my sounds under profiles. This was a step in the right direction and made it easier to tweak sounds without recompiling code. However, no matter how much I played with variables, I never was able to satisfy the complaints. 
    After I showcased my first gameplay trailer, I decided to finally sit down and really understand FMOD. I was really overwhelmed at first as I was looking at the core api, but then I was suggested to use the FMOD Studio API instead. 
    FMOD studio is a nifty program that allows much more fine control how sounds are played in a 3D environment. You can also add parameters to alter the sounds with code in real time. In this example, the engine sound can be changed via the RPM and Load values given. (You can check the examples of the SDK on how to do this.)

    FMOD Studio packs everything into bank files. You can have as many banks as you want, but for my game, I only use one. 
    To load a bank file, I just do a for loop in a directory called Media.
    void Initialize() { if (!initialized) { Msg("Initializing FMOD sound driver..."); FMOD::Studio::System::create(&system); system->getCoreSystem(&coreSystem); coreSystem->setSoftwareFormat(0, FMOD_SPEAKERMODE_DEFAULT, 0); system->initialize(1024, FMOD_STUDIO_INIT_NORMAL, FMOD_INIT_NORMAL, NULL); system->setNumListeners(1); system->update(); //Load any .bank files in main directory Leadwerks::Directory* dir = Leadwerks::FileSystem::LoadDir("Media/"); if (dir) { for (std::size_t i = 0; i < dir->files.size(); i++) { std::string file = dir->files[i]; std::string ext = Leadwerks::String::Lower(Leadwerks::FileSystem::ExtractExt(file)); if (ext == "bank") { std::string fullpath = Leadwerks::FileSystem::RealPath("Media/" + file); Msg("Loading FMOD bank file \"" + fullpath + "\"..."); FMOD::Studio::Bank* bnk = NULL; FMOD_RESULT err = system->loadBankFile(fullpath.c_str(), FMOD_STUDIO_LOAD_BANK_NORMAL, &bnk); if (err == FMOD_ERR_FILE_NOTFOUND || err == FMOD_ERR_FILE_BAD) Msg("Error: Failed to load FMOD bank file \"" + fullpath + "\"..."); } } delete dir; } initialized = true; } } Another thing to note is that you manually need to update FMOD each frame. I've created my own wrapper update function and call it in my Stage class. This also calls my Initialize function.
    void Update() { FMODEngine::Initialize(); if (!initialized) Initialize(); if (system != NULL) system->update(); } Now, that we have the basics in, we first need a listener which was actually the hardest part. At first, I wasn't sure how I can match the location and rotation calculations from Leadwerks to FMOD. I first tried implementing the position and thankfully, FMOD uses the Left-Handed system by default. 
    Rotation was a bit difficult for me but grabbing random code from stackoverflow saved me again and pushing the Y rotation 90 degrees made the 1:1 match. 
    void UpdateListenerPosition(Leadwerks::Entity* pOwner) { if (pOwner == NULL || system == NULL) return; Leadwerks::Vec3 entity_position = pOwner->GetPosition(true); // Position the listener at the origin FMOD_3D_ATTRIBUTES attributes = { { 0 } }; attributes.position.x = entity_position.x; attributes.position.y = entity_position.y; attributes.position.z = entity_position.z; float radians = Leadwerks::Math::DegToRad(pOwner->GetRotation(true).y + 90); float fx = cos(radians); float fz = sin(radians); attributes.forward.x = -fx; attributes.forward.z = fz; attributes.up.y = 1.0f; system->setListenerAttributes(0, &attributes); } There's nothing to create since a listener is initialized with the system. You just need to update its attributes each frame. I call the above function in my UpdateMatrix hook for my main camera. 
    Last, we need a way to play simple sounds. 2D and 3D sounds are defined in the bank file, but to play a sound with no 3D settings, it's as simple as the following.
    void EmitSound(const std::string& pszSoundEvent) { FMOD::Studio::EventDescription* pDest; std::string fulldsp = "event:/" + pszSoundEvent; system->getEvent(fulldsp.c_str(), &pDest); FMOD::Studio::EventInstance* instance; pDest->createInstance(&instance); instance->start(); instance->release(); } And to play a sound at an entity's location, this has been working for me so far.
    void EmitSoundFromEntity(const std::string& pszSoundEvent, ENTITY_CLASS pEntity) { FMOD::Studio::EventDescription* pDest; std::string fulldsp = "event:/" + pszSoundEvent; system->getEvent(fulldsp.c_str(), &pDest); FMOD::Studio::EventInstance* instance; pDest->createInstance(&instance); FMOD_3D_ATTRIBUTES attributes = { { 0 } }; attributes.forward.z = 1.0f; attributes.up.y = 1.0f; attributes.position.x = pEntity->GetPosition(true).x; attributes.position.y = pEntity->GetPosition(true).y; attributes.position.z = pEntity->GetPosition(true).z; instance->set3DAttributes(&attributes); instance->start(); instance->release(); } All you need to pass is the name of the event. If you decided to use subfolders, you'll need to include them in the parameter. It acts like a virtual directory.
    For anything with more control (such as loops, playback, etc) we're going to need our own Source/Speaker class.  This class just stores the event instance and acts like the standard Source/Speaker class.  I also support both the engine's OpenAL implementation and FMOD via a switch, but my actors only care about this game speaker class and the EmitSound functions. 
    class GameSpeaker { #ifdef USE_FMOD FMOD::Studio::EventDescription* m_pDest; FMOD::Studio::EventInstance* m_pEventInstance; int m_iLength; #endif // USE_FMOD Leadwerks::Source* m_pOpenALSpeaker; protected: Leadwerks::Vec3 m_vPosition; int m_iState; public: GameSpeaker(); ~GameSpeaker(); void Play(); void Stop(); void Pause(); void Resume(); void Reset(); void SetVolume(const float flVolume); void SetPitch(const float flPitch); void SetPosition(const Leadwerks::Vec3& vPosition); void SetTime(const float flTime); const float GetVolume(); const float GetPitch(); const float GetTime(); const int GetState(); const float GetLength(); const Leadwerks::Vec3& GetPosition(); void BuildSpeaker(const std::string& pszEventName); static GameSpeaker* Create(const std::string& pszEventName); static const int Stopped; static const int Playing; static const int Paused; }; extern std::shared_ptr<GameSpeaker> CreateGameSpeaker(const std::string& pszEventName); I still have very minor things to work out, but overall, this has been a success. This also could be implemented exactly the same way in Ultra Engine if you're reading this in the future. 
    Basic FMOD Implementation in Cyclone - Work in Progress - Ultra Engine Community - Game Engine for VR
  3. reepblue

    Code
    I've spent the last few months pressing buttons, clicking joysticks and shaking my computer mouse to solve the solution input for Cyclone. Back when it shipped in June, I've created a system that allowed users to assign keys to actions, in which the game would detect as input. My player code never knew what button was pressed; it just knew what action was caused. This is very similar to how Steam Input works.
    There were a few flaws with my original system. Some of which didn't surface until I shipped.
    I stored the Keycode as int32 values. Unless someone had a chart, they couldn't easily bind their keys. Things got really confusing when it came to international keyboards. Not all keycodes are universal. My system only supported buttons. Actions for axis controls were not a thing and were hard coded. The system relied on the Leadwerks API which would cause making a utility app kind of tricky. Mouse aim wasn't generally liked. I wanted a library that did nothing but input. But I got dead libraries or libraries that needed dependencies to work. I wanted Steam Input, but for the Keyboard and Mouse. I knew I couldn't let this sit based on the amount of feedback I was getting because from this. Since nobody else thought it was necessary to make one, it looked like I had to make an input system in-which input has a name.
    Before we get into input, I first had to re-arrange how my repo file structure was set up. I had Cyclone set up as a generic Leadwerks project, but this wasn't going to work if I wanted to create shared libraries and utilities.  Before I did anything stupid with Cyclone, I made a new repo to figure out how everything should be laid out. I decided to follow a file structure much like Valve has their Source engine and use premake to generate the files. Shell scripts are used to generate projects via WSL. This allowed me to compile for Windows AND Linux on the same machine and I did casual build tests on macOS. I never want to write an input system ever again.
     
    Before I could do any form of action detection, I needed to create a "Driver" sort of speak and have the operating system pump events into it. This interface class allows classes to be derived from it and work no matter what driver is created.
    class INPUTSYTEM_API IInputDriver { public: IInputDriver() {}; virtual ~IInputDriver() {}; virtual void EnablePumpEvents(const bool bState) = 0; virtual void PumpButtonDown(const button_t btn, const int controller = 0) = 0; virtual void PumpButtonUp(const button_t btn, const int controller = 0) = 0; virtual void PumpButtonCodeDown(const ButtonCode& btncode, const int controller = 0) = 0; virtual void PumpButtonCodeUp(const ButtonCode& btncode, const int controller = 0) = 0; virtual void PumpMouseWheelDir(const int dir) = 0; virtual void PumpAxis(const AxisCode& axiscode, const float x, const float y, const int controller = 0) = 0; virtual void PumpPointer(const PointerCode& pointer, const int x, const int y, const int controller = 0) = 0; virtual void PumpRumble(const float left, const float right, uint64_t duration_ms, const int controller = 0) = 0; virtual const bool ButtonDown(const ButtonCode& btncode, const int controller = 0) = 0; virtual const bool ButtonHit(const ButtonCode& btncode, const int controller = 0) = 0; virtual const bool ButtonReleased(const ButtonCode& btncode, const int controller = 0) = 0; virtual const bool ButtonAnyDown() = 0; virtual const bool ButtonAnyHit() = 0; virtual AxisVector GetAxis(const AxisCode& axiscode, const int controller = 0) = 0; virtual AxisVector GetButtonAxis(const ButtonCode& up, const ButtonCode& down, const ButtonCode& left, const ButtonCode& right) = 0; virtual void UpdateController(const int controller) = 0; virtual void SuspendControllerInput(const bool bState) = 0; virtual void Flush() = 0; virtual void FlushButtons() = 0; virtual void FlushAxis() = 0; virtual ButtonCode GetLastButtonPressed() = 0; virtual ButtonCode StringToButtonCode(const std::string& btnstring) = 0; virtual const char* ButtonCodeToString(const ButtonCode& btncode) = 0; virtual void SetCursorPosition(const int x, const int y) = 0; virtual void CenterCursorPosition() = 0; virtual ScreenPosition GetCursorPosition() = 0; virtual void MouseVisibility(const bool bState, const bool bRecenter) = 0; virtual void SetPointerPosition(const PointerCode& pointer, const int x, const int y) = 0; virtual ScreenPosition GetPointerPosition(const PointerCode& pointer) = 0; virtual void CenterPointerPosition(const PointerCode& pointer) = 0; virtual void SetActiveDevice(InputDevice device) = 0; virtual InputDevice GetActiveDevice() = 0; virtual bool DeviceChanged() = 0; virtual void Cleanup() = 0; }; You may notice that there is button_t and ButtonCode. The type: button_t is the unit32_t type from the OS and the ButtonCode is button_t reassigned to my own enum structure. This way, my end values remain consistent between OS and drivers. For example, WinAPI pumps Virtual Keyboard flag values while Coco/X11 will pump its own flag definitions. If you wanted to use SDL, all you need to do is make an SDL driver for it and the code on top of it will not care.
    Also note that there are no separate functions to detect button and key presses. There are only Buttons, Axis, and Pointer values when it comes to my input library. I was also going to omit the cursor commands, but it's part of the desktop environment and really can't be ignored. The Set/GetCursorPosition is there for redundancy really. 
    The pump functions are made public to allow external pumping of events. For Windows, you can "hijack" the Window polling with a custom function. Here's an example of how I did it in Leadwerks and the same thing can be done in Ultra Engine for Windows exclusive games.
    LRESULT CALLBACK MyWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { InputSystem::ProcInputWin32(hwnd, message, wparam, lparam); return Leadwerks::WndProc(hwnd, message, wparam, lparam); } int main(int argc, const char* argv[]) { auto window = Leadwerks::Window::Create("Game", 0, 0, 1280, 720, Leadwerks::Window::Titlebar | Leadwerks::Window::Center); if (window == NULL) return 1; // Init the input system. InputSystem::Init(window->hwnd); #ifdef _WIN32 // Swap the callback with ours WNDPROC OldProc = reinterpret_cast<WNDPROC>(SetWindowLongPtr( hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(MyWndProc))); #endif .... That's all it takes to start my input system. Just pass the window handle to the Init function and the library will load the rest. I only have a WinAPI driver right now, but the idea is that of you were building on Linux or macOS, the driver will be created. I still need to work out how polling and key events are processed on those platforms.
    We can get a button press as easily as this:
    auto b = InputSystem::GetDriver()->ButtonHit(InputSystem::BUTTON_KEY_ESCAPE); Josh provided me with insight on using the raw mouse value to return a Vector valve so you can easily obtain it by:
    auto a = InputSystem::GetDriver()->GetAxis(InputSystem::AXIS_MOUSE); You can even do neat things like this:
    if (InputSystem::GetDriver()->ButtonAnyDown()) { auto btn = InputSystem::GetDriver()->GetLastButtonPressed(); std::cout << InputSystem::GetDriver()->ButtonCodeToString(btn) << " is held down!" << std::endl; }  
    Great, we now have an input API like everyone else. Although it's richer and more flexible than what Leadwerks provides, we're not done. We need another layer on top of the driver in which our game will actually use.
    Meet the Action Controller!
    class INPUTSYTEM_API IActionController { public: int32_t controller_port = 0; IActionController() {}; virtual ~IActionController() {}; virtual const int32_t GetControllerID() = 0; virtual void SetActionSet(const char* actionsetid) = 0; virtual const char* GetActionSet() = 0; virtual bool Down(const char* actionid, const char* actionsetid = "") = 0; virtual bool Hit(const char* actionid, const char* actionsetid = "") = 0; virtual bool Released(const char* actionid, const char* actionsetid = "") = 0; virtual AxisVector Axis(const char* actionid, const char* actionsetid = "") = 0; virtual void Rumble(const float leftmotor, const float rightmotor, uint64_t duration_ms = 10) = 0; virtual void FlushAllInput(const bool bCenterPointDevice = false) = 0; virtual const bool NoActionSets() = 0; virtual const Action GetAction(const char* actionid, const char* actionsetid = "") = 0; virtual const int ButtonCount(const char* actionid, const char* actionsetid = "") = 0; virtual const int AxisCount(const char* actionid, const char* actionsetid = "") = 0; virtual const ButtonCode GetButton(const char* actionid, const int index = 0, const char* actionsetid = "") = 0; virtual const AxisCode GetAxis(const char* actionid, const int index = 0, const char* actionsetid = "") = 0; virtual const ButtonAxis GetButtonAxis(const char* actionid, const char* actionsetid = "") = 0; virtual const PointerCode GetPointDevice() = 0; virtual const bool IsKBM() = 0; virtual const float GetSetting(const char* setting, const float defaultsetting = 0) = 0; virtual void SetPointDevice(const PointerCode& pointdevice) = 0; virtual void SetPointDevicePosition(const int x, const int y) = 0; virtual ScreenPosition GetPointDevicePosition() = 0; virtual void CenterPointDevice() = 0; virtual void TogglePointerVisibility(const bool bShow) = 0; virtual const bool GetPointerVisibility() = 0; // Writting of data virtual void SetSetting(const char* setting, const float fValue) = 0; virtual void RegisterActionSet(const char* actionsetid) = 0; virtual void BindAction(const char* actionsetid, const char* actionid, const ButtonCode& buttoncode) = 0; virtual void BindAction(const char* actionsetid, const char* actionid, const AxisCode& axiscode) = 0; virtual void BindAction(const char* actionsetid, const char* actionid, const ButtonAxis& buttonaxis) = 0; virtual void ClearAction(const char* actionsetid, const char* actionid) = 0; virtual IInputDriver* GetDriver() = 0; friend class IInputDriver; };  
    Did notice that we pass strings instead of button codes? So instead of checking if the Space bar is pressed, we check if the Space action is pressed.
    // Bad, don't do this. const bool b = InputSystem::GetDriver()->ButtonHit(InputSystem::BUTTON_KEY_SPACE) if (b) pPlayer->Jump(); // Do this! auto controller = InputSystem::GetDriver()->GetController(); if (controller->Hit("Jump")) pPlayer->Jump(); The keys can be ether assigned in code or loaded form a JSON file. All actions are converted to lower case to prevent confusion between "Jump" and "jump". Actions can have multiple buttons binded to it which is good for also storing controller buttons. The controller also supports having 4 buttons act as an axis which should be used for movement.
    It's easy to create an action controller. 
    auto action_controller = InputSystem::CreateActionController("actioncontroller.json"); Don't have a script yet? You can directly bind buttons and axis values after its creation and save the results to get started.
    auto action_controller = InputSystem::CreateActionController(""); const char* action_set = "TestActionSet"; action_controller->RegisterActionSet(action_set); action_controller->BindAction(action_set, "Camera", InputSystem::AXIS_MOUSE); action_controller->BindAction(action_set, "Camera", InputSystem::AXIS_GAMEPAD_RSTICK); action_controller->BindAction(action_set, "Jump", InputSystem::BUTTON_KEY_SPACE); action_controller->BindAction(action_set, "Jump", InputSystem::BUTTON_KEY_C); action_controller->BindAction(action_set, "Jump", InputSystem::BUTTON_GAMEPAD_A); InputSystem::ButtonAxis move_axis = { InputSystem::BUTTON_KEY_W, InputSystem::BUTTON_KEY_S, InputSystem::BUTTON_KEY_A, InputSystem::BUTTON_KEY_D }; action_controller->BindAction(action_set, "Move", move_axis); action_controller->BindAction(action_set, "Move", InputSystem::AXIS_GAMEPAD_LSTICK); InputSystem::SaveControllerProfile(action_controller, "inputtest.json");  
    This is all well and good, but the real payout will be to have an app that can easily bind raw input to actions. For this, I had to use Ultra App Kit as I needed something compatible with my Win32 libraries. Otherwise, I would have used the full engine.
    Like I said, it's pretty straight forward to hook this up with the Ultra API.
    #ifdef _WIN32 LRESULT CALLBACK MyWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { InputSystem::ProcInputWin32(hwnd, message, wparam, lparam); return UltraEngine::Window::WndProc(hwnd, message, wparam, lparam); } #endif std::shared_ptr<UltraEngine::Window> BuildWindow(const int w, const int h) { //Get displays auto displays = GetDisplays(); //Create window auto mainwindow = CreateWindow("Action Mapper", 0, 0, w, h, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR | WINDOW_HIDDEN); mainwindow->SetMinSize(w, h); #ifdef _WIN32 // Get device context HWND hwnd = mainwindow->GetHandle(); HDC hdc = GetDC(hwnd); // Load the icon for window titlebar and taskbar HICON icon = LoadIconA(GetModuleHandle(NULL), (LPCSTR)101); SendMessage(hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(icon)); // Swap the callback with ours WNDPROC OldProc = reinterpret_cast<WNDPROC>(SetWindowLongPtr( hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(MyWndProc))); #endif InputSystem::Init(); InputSystem::GetDriver()->EnablePumpEvents(false); InputSystem::GetDriver()->SuspendControllerInput(true); return mainwindow; } I first initialize the driver, then tell it to not pump any OS events into it. This is because I wanted the events to pump to my driver only when requested by my bind button class. When that is pressed, the button gets disabled until InputSystem::GetDriver()->ButtonAnyHit() returns true and saves the last pressed key. I didn't need to pass the window handle as that's only needed to toggle the visibility of the cursor. 

    This ended up working really well, and I'm happy how it came out. But you know what would be really customer friendly? What if this had a native Linux build? Ultra App Kit has Linux64 libraries, and my entire build environment uses premake with WSL so building one was no issue. The issue is that I didn't look into X11 and I don't wanna hold the next update much longer.
    Using my input library, I just pump the events from the Ultra API into my WinAPI driver and most keys worked. I had to fix Control, Alt, and Shift, but it was pretty easy. This only gets used with the Linux build. 
    #ifdef __linux__ #define VK_LSHIFT 0xA0 #define VK_RSHIFT 0xA1 #define VK_LCONTROL 0xA2 #define VK_RCONTROL 0xA3 #define VK_LMENU 0xA4 #define VK_RMENU 0xA5 #endif namespace UltraInputWrapper { // We need to convert some keys to translate // engine values to Windows VK values. const int Convert(const int in) { int key = in; if (in == UltraEngine::KEY_CONTROL) return VK_LCONTROL; //if (in == UltraEngine::KEY_CONTROL) return VK_RCONTROL; if (in == UltraEngine::KEY_SHIFT) return VK_LSHIFT; //if (in == UltraEngine::KEY_SHIFT) return VK_RSHIFT; if (in == UltraEngine::KEY_ALT) return VK_LMENU; //if (in == UltraEngine::KEY_ALT) return VK_RMENU; return key; } // For non-Windows, push engine values. // Since UltraEngine uses the same values as WinAPI, it should be fine.. void PollIntoDriver(const Event& e) { switch (e.id) { case EVENT_KEYDOWN: InputSystem::GetDriver()->PumpButtonDown(Convert(e.data)); break; case EVENT_KEYUP: InputSystem::GetDriver()->PumpButtonUp(Convert(e.data)); break; case EVENT_MOUSEDOWN: InputSystem::GetDriver()->PumpButtonDown(Convert(e.data)); break; case EVENT_MOUSEUP: InputSystem::GetDriver()->PumpButtonUp(Convert(e.data)); break; default: break; } } }  
    The final step was to gut the old input system out of Cyclone and replace it with my action controller. I also had to make it co-exist with Steam Input which I found out nulls out my XInput calls. I wrote a new singleton class that checks the state of both my action controller and Steam Input. Then it was making sure the right glyphs showed up per action.
    One last thing I want to share is a little bit of how my GameController class works. Since this follows the same ideology as Steam Input, it all works nicely.
    // Button Example const bool GameController::OnJump() { if (actioncontroller == NULL) return false; return actioncontroller->Hit("Jump") || SteamInputController::Hit(SteamInputController::eControllerDigitalAction_Jump); } // Axis Example Leadwerks::Vec2 GameController::GetMovementAxis() { Leadwerks::Vec2 ret = Leadwerks::Vec2(0); if (actioncontroller != NULL) { // Shared movement between KB and Controller. InputSystem::AxisVector moveAxis = actioncontroller->Axis("Move"); ret = Leadwerks::Vec2(moveAxis.x + SteamInputController::Axis(SteamInputController::eControllerAnalogAction_MoveControls).x, moveAxis.y + SteamInputController::Axis(SteamInputController::eControllerAnalogAction_MoveControls).y); } ret.x = Leadwerks::Math::Clamp(ret.x, -1.0f, 1.0f); ret.y = Leadwerks::Math::Clamp(ret.y, -1.0f, 1.0f); return ret; }  
    This was an absolute time vampire, but I'm glad it's done minus the Coco/X11 drivers. Cyclone will be updated soon with this and hopefully this fixes international bindings. I couldn't find any information on the subject, and my virtual Turkish keyboard works properly. I'm just hoping the storing of the VK values is enough.
    I can easily build this for Windows, Mac and Linux to work with Leadwerks or the upcoming Ultra Engine. This is how we should be programming our input for our games. Stop using window->KeyHit(). I plan on releasing the binaries so everyone can have better input code. 
     
×
×
  • Create New...