Jump to content

reepblue

Developers
  • Posts

    2,480
  • Joined

  • Last visited

Blog Entries posted by reepblue

  1. reepblue
    Back in July, I set out to create a base for creating a version of Cyclone with Ultra Engine. I've gotten a lot of feedback from the release and started to conceptualize ideas on how to prevent or limit on making the same mistakes.
    One major goal I had was to compartmentalize the system. This should just exist on it's own and game code should just lay on top of this. This was possible thanks to the event system which cuts down on the number of pointers that need to be passed. Components can just listen to events from the program and act accordingly. 
    Lastly, I've made the decision to open source this on GitHub as this is the kind of thing that users think about when it's too late. People just want to work on their cool games and not worry about the window or if a setting will be applied correctly. 
    So here are 6 reasons/features of the Ultra Game System! 
    1. Window Management
    One of the top complaints I got with Cyclone is that users could not resize the window in-game. This was an engine limitation of Leadwerks This is now possible in Ultra Engine but it needs some elbow grease to make it work. The Game System does this for you. All you need to do is call GetProgram()->ResizeApp() and the graphics window class will recreate the window and framebuffer for you. By default, pressing F11 will swap between windowed mode and full screen. 
    2. Out-Of-Game Settings
    The Game System has an isolated window application for changing settings. The idea behind this is so there's a way to modify any window or graphics setting if an in-game one is unavailable. You probably shouldn't ship with just this solution as many people prefer to edit any setting within the game itself. 
    This is accessible by using the -settings flag with the program.
    3. Action Based Input System
    Years of research into this paid off when the Input Update for Cyclone released. The knowledge was carried over for the Game System. Only this time it's more dynamic! 
    Simply set the controls in your main.cpp file:
    // Define default controls. static void InstallControls(shared_ptr<GameController> controller) { if (controller == NULL) return; // Program actions controller->SetAction("Pause", BUTTON_KEY_ESCAPE); controller->SetAction("Terminate", BUTTON_KEY_END); controller->SetAction("ConsoleApp", BUTTON_KEY_F1); controller->SetAction("Fullscreen", BUTTON_KEY_F11); controller->SetAction("Screenshot", BUTTON_KEY_F2); controller->SetAction("Quick Save", BUTTON_KEY_F5); controller->SetAction("Quick Load", BUTTON_KEY_F6); // Camera ButtonAxis moveaxis = { BUTTON_KEY_W, BUTTON_KEY_S, BUTTON_KEY_A, BUTTON_KEY_D }; controller->SetAction("Movement", moveaxis, "InGameControls"); controller->SetAction("Camera", AXIS_MOUSE, "InGameControls"); controller->SetAction("Sprint", BUTTON_KEY_SHIFT, "InGameControls"); controller->SetAction("Crouch", BUTTON_KEY_CONTROL, "InGameControls"); controller->SetAction("Climb", BUTTON_KEY_Q, "InGameControls"); controller->SetAction("Desent", BUTTON_KEY_E, "InGameControls"); controller->SetAction("Jump", BUTTON_KEY_SPACE, "InGameControls"); // Settings controller->SetSetting("Raw Mouse", false); controller->SetSetting("Inverse Mouse", false); controller->SetSetting("Mouse Smoothing", 0.0f); controller->SetSetting("Mouse Look Speed", 1.0f); } Then deriving your components off of the GameObject class, you can use GetInput() for input functionality. 
    virtual void UpdateInput() { // Movement if (allowmovement) { float speed = movespeed / 60.0f; if (GetInput()->Down("Sprint")) { speed *= 10.0f; } else if (GetInput()->Down("Crouch")) { speed *= 0.25f; } if (GetInput()->Down("Climb")) GetEntity()->Translate(0, speed, 0); if (GetInput()->Down("Desent")) GetEntity()->Translate(0, -speed, 0); auto axis = GetInput()->Axis("Movement"); GetEntity()->Move(axis.x * speed, 0, axis.y * speed); } } Best part is the "Controls" tab will reflect whatever you have defined!

    4. User Input via Console
    Having a developer console is essential for developing any game! The Game System has a very simple but flexible console that doesn't need any commands registered beforehand. To define a new command, just poll the EVENT_CONSOLEEXECUTE id in your component's ProcessEvent function.
    virtual void Start() { Listen(EVENT_CONSOLEEXECUTE, GetProgram()); } virtual bool ProcessEvent(const Event& e) { if (e.id == EVENT_CONSOLEEXECUTE) { auto line = e.text.Split(" "); auto cmd = line[0].ToString(); if (line.size() > 1 && !line[1].empty()) { if (cmd == "crosshair") { bool hide = (bool)line[1].ToInt(); hudcamera->SetHidden(!hide); Print(QuoteString(cmd) + " has been set to: " + line[1]); } } } } 5. Sound Managment
    A layer of functionality in the Game System allows for cleaner sound origination and playback. You can store sound variables (files, volume, pitch, range, etc) within a JSON script.
    { "audioProfile": { "file": "Sound/radio_dreamlandloop_mono.wav", "volume": 0.5, "range": 35.0, "loop": true } } Then load the file with the GameSpeaker class.
    auto file = "Sound/Profiles/Radio.json"; shared_ptr<GameSpeaker> speaker = CreateGameSpeaker(file, GetEntity()->GetPosition()); The GameSpeaker has Save/Load functions so the speaker's time and state can be restored. Here's an example of creating and restoring a GameSpeaker.
    virtual bool Load(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const LoadFlags flags) { Print("Loading component " + QuoteWString(name)); if (speaker) { if (!properties["componentspeaker"].is_null()) speaker->Load(properties["componentspeaker"], binstream, scene, flags); } else { auto file = "Sound/Profiles/Radio.json"; speaker = CreateGameSpeaker(file, GetEntity()->GetPosition()); if (!properties["componentspeaker"].is_null()) speaker->Load(properties["componentspeaker"], binstream, scene, flags); } return true; } virtual bool Save(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const SaveFlags flags) { properties["componentspeaker"] = {}; if (speaker) speaker->Save(properties["componentspeaker"], binstream, scene, flags); return true; } As of right now, the sound system doesn't support audio filters as I feel that the filter should be applied within a volume (or to the listener) and not the sound/speaker itself. I'm still thinking about how that should work. 
    6. Better Render Layer Managment
    Intergrated today, The Canvas class is a great way to ensure that there is one camera per render layer. I ran into an issue where a component would draw 2D graphics to the framebuffer, but when there were multiple instances of them, multiple cameras were being made and drawing over each other. Using GetCanvas() can prevent this from happening.
    auto canvas = GetCanvas(world, RENDERLAYER_HUD); auto sprite = CreateSprite(world, 1.0f, 1.0f); sprite->SetPosition((float)sz.x / 2, (float)sz.y / 2); RenderToCanvas(sprite, canvas); The Game System will be my main focus until the Ultra Engine version of Cyclone starts development which will use this as a foundation.  I'll also be working on other small things to share so be on the lookout for those! 
  2. 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. 
     
  3. reepblue
    With the new engine coming along and me noticing limits with Cyclone's current architecture, it's time for me to start thinking about the mistakes I've made and how would avoid repeating them with the new engine. Luckly, Ultra Engine is far more flexible than Leadwerks thanks to smart pointers and the event system. I wish to share some of my plans going forward.
    Log, Log, Log Everything!
    I personally never look at my log files. I instead look at the CMD output and the debugger to see what's going on. However, end users don't have that luxury. The engine will automatically log its warnings and error messages, but it'll not tell you when that message was printed or the events that led up to it. Creating your own log stream like this will give you far more control over your log file.
    // Prepare the log file. auto logfile = WriteFile("Game.log"); // Main loop bool running = true; while (running) { while (PeekEvent()) { const auto e = WaitEvent(); if (e.id == EVENT_PRINT) logfile->WriteLine(e.text.ToString()); } } logfile->Close(); logfile = NULL; You can easily do add time stamp before the message, so you have an idea when the event actually happened. You can use real world clock time or the current application time.
    Do Nothing OS Specific (Unless It's Aesthetic)
    Cyclone is a Win32 application. Period. It works wonderfully on Proton and there isn't any reason for me to install an outdated version of Ubuntu and waste time making a native build for Linux that'll only sometimes work. I took the liberty of using the Windows API to create my own Input System, XInput integration, and the Splash Window. I also created a custom DLL for my Input System so the Action Mapper application would use the same system as the game. 
    Going forward, I want my code to work wherever Ultra Engine can run on. This means sticking to the API for mostly everything unless I need to use a 3rd party library (Like Steamworks). Still, 98% of your users will be using Windows so you might as well add some nice touches like the application knowing what theme the user is using and dressing your application accordingly like this.
    Launch Arguments != Settings
    If you've programmed with Leadwerks, you should be familiar with System::SetProperty() and System::GetProperty(). This by default will register a launch argument as a setting and then save it to a settings file. Cyclone works like this too, but arguments don't get saved to the settings file to avoid confusion. 
    I never used arguments to override settings. I only used -devmode, -console, -fullscreen, +screenwidth and +screenheight. So, instead of mixing launch arguments with user settings, I think just going to keep them separated and only use the arguments to tell the application how to launch. 
    Everything Will Be One
    Cyclone ships with 2 applications. One being the main game, and the other being the Action Mapper for key binding. They both share a dynamic library. This is because Cyclone is made using Leadwerks and the Action Mapper is made using Ultra App Kit. I want to make a Console window to remove it from the UI and a dedicated window for graphic settings would be handy. My new approach will make these separate windows that can be called with a launch command. 
    Compartmentalize Everything
    I ran into many conflicts when adding working on the Flag Run update. Cyclone was structured to be a Puzzle game, and here I was adding clocks and an auto reload system. I had to do a lot of edits to my code to make it work and not break the older maps. 
    Because of how Leadwerks worked, I had to do a lot of mixing and cross referencing of classes. In the end, it's all very messy now and it discourages me from adding to it more. My game application code will only create the window, framebuffer and camera. It's also will be responsible handle scenes and emit events. The components should do everything else. I plan to make a component for the always existing camera entity and have it listen to events when to show UI elements or change its settings. I can make the UI and settings components separate too! 
    Since there will be multiple "apps" in one application, I kind of have to do this. Thankfully, I don't have to do things like create my own reflection system this time...
    Get The Essentials Right, The First Time
    When you're making your first game, you generally want it up and running as soon as possible. Everything else critical you'll say "Meh, I'll worry about that later". Why would you spend time on things like an Input System or A Sound System if you don't even know the game is fun? A month or so later, you find yourself figuring out how the new systems you just built should be integrated with the existing game code. It's not funny, it's not fun. 
    I already have a game, and I know what works so I don't need to repeat that again. And being that the engine isn't 100% yet, I have plenty of time to get the essentials right. Here is what I'll be spending the first few months on.
    Ensuring that windows display and activate correctly. A splash window should show up, and the next window should be brought forward 100% of the time. Cyclone has a bug right now which a game window doesn't get brought to the top although I'm using every API call to make it do so. Settings system should be in-place before drawing a 3D cube to a framebuffer.  I have a very good input system in Cyclone already, but it relies heavily on WinAPI to work. I also include XInput code which gets overwritten by Steam Input anyway. The input system will now need to use the engine's API instead of Windows.  This system will work exactly the same as I have it today, but I think I'm going to leave out controller integration for now. I really liked using FMOD, but to lower dependencies, I think I should try the new OpenAL-Soft sound engine. I plan for my new system to read json scripts for sound entries that contain data for the Speaker and close captioning to display on the screen.    I plan to get cracking within a few weeks! I set up my PC with the RTX 3600 running Windows 10 to develop and stream to Discord! I noticed a lot of slow down with my GTX 1050 running the engine in fullscreen so it's time to use something with more power. I am keeping an eye out for a good price on a 4070 Ti though.  
  4. reepblue

    Cyclone
    As a teenager, I've spent thousands of hours with the Source SDK. It was really cool how Valve gave people the real tools to develop and create maps and mods. Overtime I got to see and take apart games to see how they actually work.
    Overtime, the indie engine market took over, and making Source mods is now just a terrible experience. Hammer crashes more frequently and the SDK branch of the engine has old bugs that were never patched. I want the excitement I had of making maps and cool features to be part of Cyclone as a package. Leadwerks has a very intuitive interface, and with a Lua Sandbox, many possibilities open up.
    As of the last update shipped today, anyone with a license of Leadwerks Game Engine has the ability to create custom maps and scripts and share them with others. This is the first step to see if anyone would be interested if they had the tools in their holster.
    There's now a batch file called "create_mod_env.bat" which will create a Leadwerks project within the game directory by default. You can edit the batch file to define a different path if you wish. You'll also receive a few example maps showing the logic of the game. Import the project in Leadwerks, and you should be good to go! When you're ready to share, zip up the map and chapter script and tell people to install it in a folder called "Addons" within the game's directory.
    While this is only the first draft of this, I'm interested to see where it will end up. I hope you all find it interesting and motivating.
  5. reepblue
    After sixteen months of week-by-week development, I'm happy to announce that the anticipated spiritual successor of the Portal mod, Blue Portals is now available for sale as an Early Access title on Steam!
     
    I want to take a moment to thank [b]everyone[/b] who has played the demo and gave feedback. There's still work to be done! Being an Early Access title allows Cyclone to continue keep players involved in the development process to help it evolve into the best it can be! 
    I hope you're all excited as I am to see where Cyclone will be in the near future!
     
    Please enjoy!
     
  6. reepblue

    Cyclone
    From its initial concept in 2017 through the last sixteen months of development, I am ecstatic to announce that the free demo of Cyclone is now available in preparation for the June Steam Next Fest!  This demo includes three maps from the main game showcasing the functionalities of Cyclone and various puzzle elements.  
    The demo is available right on the Steam Store page!
    Cyclone on Steam (steampowered.com)
    While this is both an exciting and a scary time. I can't stress enough that your feedback will be very helpful as we get closer to the Early Access release date of June 23rd, 2022. Please let me know what you think publicly right here on UltraEngine.com, privately via a DM or my inbox of sferriolo@reepsoftworks.com.


  7. reepblue
    I'm excited to announce that Cyclone will be participating in Steam Next Fest 2022! While I'll not be live streaming, a playable demo will be available during the festival. The demo will act like an even softer release than the Early Access release, which should roll into the planned release date of June 23rd, 2022!

     
    Add it to your wish list on Steam: https://store.steampowered.com/app/1803590/Cyclone/
    Check out the itch.io Page: https://reepblue.itch.io/cyclone
    Join my public Discord: https://discord.gg/Hh7Ss9fWaW
  8. reepblue

    Cyclone
    The time finally came to revamp the main menu for Cyclone. Doing menu work for me is really hard to get into but once I'm knee deep into it, it's all I want to do. I decided now is the time to work on a semi-final menu system due to the following.
    I just finished Steam Input and the menu wasn't compatible. The white text on a bright background was hard to make out. I only had a "New Game" option. There was no way to jump to a map you wanted to play without the console enabled. The first step was to figure out how to integrate the GUI with Steam input. The Leadwerks GUI system is from my experience is an unfinished predecessor of the same GUI calls of the Ultra App Kit. The input was really for a mouse and keyboard but I didn't want to bother learning imgui or another API. One thing that made it really complex is that Leadwerks uses Lua scripts for the GUI draw and event queue commands, 
    In the end, I just did a Steam Input binding to control the mouse with the touchpad or a joystick and told the OS to simulate the left mouse being pressed when the action is called. I don't know if this will get me any approval badges, but now you can control the menu with just the controller. The extra parameters don't seem to do anything but I kept them in. Regardless, it works. We don't have to call any "Legacy Keys" for the bindings!
    //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CycloneMenu::OnUpdate() { if (!Hidden()) { if (Input::ActionHit(ACTION_MENUSELECT)) { EventQueue::Emit(Event::MouseDown, Window::GetCurrent(), Key::LButton, Window::GetCurrent()->GetMousePosition().x, Window::GetCurrent()->GetMousePosition().y); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CycloneMenu::ProcessEvent(const Leadwerks::Event iEvent) { ... else if (iEvent.id == Event::MouseDown) { // A hacky way of doing this. #ifdef _WIN32 INPUT Inputs[3] = { 0 }; Inputs[0].type = INPUT_MOUSE; Inputs[0].mi.dx = Window::GetCurrent()->GetMousePosition().x; // desired X coordinate Inputs[0].mi.dy = Window::GetCurrent()->GetMousePosition().y; // desired Y coordinate //Inputs[0].mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE; Inputs[1].type = INPUT_MOUSE; Inputs[1].mi.dwFlags = MOUSEEVENTF_LEFTDOWN; Inputs[2].type = INPUT_MOUSE; Inputs[2].mi.dwFlags = MOUSEEVENTF_LEFTUP; SendInput(3, Inputs, sizeof(INPUT)); #endif ... } } Now, controlling the mouse with a joystick isn't the best approach. I also had to make my buttons much bigger so it was harder to miss the bounds of the button when using the controller. I also kept all my UI elements in the center of the screen.


    Next was the options menu. I used choice boxes on everything to make it less Mouse centric,

     
    Finally came the new "Play" menu which I wanted to revamp from just a simple button that would load the "start" map. Right now, I'm forecasting only to ship with 10 maps but what about any new maps later on? Would I just add it onto the 10? What if I want to make a set of maps that focus on a theme or a new element? Would that break the progression? What about when custom maps are loaded? People would be going to the "Custom Maps" box more than the "New Game" button by then. It's not like it's a story or anything to justify those maps being held at a different standard than other maps.
    I decided to take a similar approach to the campaigns in Left 4 Dead. The chapters shipped with the game are loaded into the menu first. Then the application looks for anything under the addon's folder and loads those in. Each chapter can have a collection of maps that are easily selected by the user. 

    { "name": "Cyclone 101", "author": "Reep Softworks", "maps": { "Level 0": { "filepath": "Maps/lvl0.map", "imagepath": "Materials/GUI/background_lvl0_artpass1.tex" }, "Level 1": { "filepath": "Maps/lvl1.map" }, "Level 2": { "filepath": "Maps/lvl2.map" } } } Custom maps work the same way no special treatment. This all works with a json script and the application can locate the script in the zip package. Also, if no image is defined for a map, a "NO IMAGE" texture substitutes in.  Just click start, and off you go!


     
    { "name": "MyCoolAddon", "author": "reepblue", "maps": { "The Cool Addon Map": { "filepath": "Maps/addontest.map", "imagepath": "Materials/GUI/loadingscreen.tex" } } } One thing I'm not sure about is splitting my 10 maps in 3 chapters. I did this after feedback of the list being so empty with just one entry. Hopefully I'll have time to make new maps. Being that now I'm free to work on small collections instead of one long track should be motivating. A goal of arranging the maps like this is so I can justify the game based on the amount of content over "how long" it is. 
    It should be really easy to integrate Steam Workshop with this but I'm not going worry about it now, Tonight I'm happy I can play Cyclone with my Steam Controller without reaching for the mouse and keyboard!
  9. reepblue

    Cyclone
    It's been roughly over a year since active development started on this project. Today, I'm excited to announce that Cyclone now has a "Coming Soon" page on the Steam Store! This marks a huge milestone for the project and I'm looking forward to getting this out as soon as possible. 
    Cyclone is planned on being released as an Early Access title. Releasing into Early Access will grant a layer of feedback, bug reports, and ideas that can help build the remainder features, as well as additional content. So much was done, yet there's much more to do before the planned Early Access release scheduled for Q2 2022! 
    If you are looking forward to Cyclone, please consider adding it to your wishlist and/or giving it a follow to be informed about future updates. 
     
  10. 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
  11. 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!) 
     
  12. reepblue
    In late 2014, I made the bold decision to focus less with Source Engine mods and pick up an actual engine I could use without legal conflicts or worrying about knowing the right people. At the time, it was like all other options were telling me to forget everything I've picked up through the years of making popular maps and mods and learn something that's completely unique to their software. CSG tools were declared too uncool, and any engine that was nice enough to support it enforced that any and all CSG objects should be removed.
    And there I found Leadwerks. The editor looked and acted like Valve's Hammer Editor and I could code my game in C++. While the Source Engine didn't really use modern C++ (It was more C with classes and a bunch of macros if anything anything) I felt more open to learn this than anything else at the time. Also, my internet wasn't great at the time so the small install size was a plus.
    Only thing that threw me off from the start was that a lot wasn't done for you. With Leadwerks, YOU are in charge of window creation and YOU are in charge of how maps are loaded and cleared. Any engine I've tried, this is done for you and they give you documentation on how their system works for you to read which I'm not a big fan of. 
    After I shipped the Vectronic Demo in 2015, I wanted to have a nice C++ foundation framework to work on. Back then, there wasn't an Actor class to attach C++ functionality to entitles, the animation functionality was in a Lua script, and back then there was no editor materials so you had to make trigger boxes invisible. I wanted something that didn't need Lua to function and make life easier to make maps. I also wanted code for window and world handling to be automatic- to "just work", This started as the LEX template. 
    Looking back, the LEX template is a huge mess of code that did all of that. A demo game was made for a game tournament in 2015 or 2016 (I forgot tbh) called Crawler's Den. The entire game was written in C++. During the development time, my animation code I translated from the Lua variant got adapted into the base engine which made life much easier. This also made animations faster and easier for everybody. I also took time to look into cubemap reflections which I think really pushed for the probes in 4.1. In the end, the template was unstable and hard to work with. I was often afraid to clear and load a new map due to raw pointer issues.
    I instead dropped the idea in favor of a similar system using Lua. Luawerks was everything I wanted LEX to be but much safer. It was more adoptable as most people only have the Standard Edition of Leadwerks. I finished the code and pushed it to the market place.  
    However, I really wanted to make a game with C++ and at this point VR was coming online which C++ is better suited for. 
    For the past 3 or so years, I was writing testing, scrapping and recycling code figuring out the "best" way of a system. I tried to do a translation of Luawerks to C++ but decided against it early on. I figured out why my code was crashing before, what objects I can Release when the entity deletes and overall became better at programming. However, a new engine was on the way and I didn't want to write code if I just had to redo it again.
    I threw the 5 bucks a month for the early subscription at the time for access of the SDK of the new engine. Back then, I thought the new engine was just going to be "Leadwerks but faster" but that wasn't the case. I made a few macro's and #ifdef's to accommodate for this. Over time however, function names were changed, renamed, or removed completely. and the new engine slowly began to do things differently than Leadwerks. 
    It came to a point in which how my systems would work needed to be completely different for both engines. In late 2019, I decided to cut the code in half making one folder called "Core" for core app functionality. That part of the code would be translated for the new engine and tested occasionally. This is also the time when I decided to really look into input functions. The "Game" code was code I would just create as normal and worry about it converting it later a file at a time.  You can play the results here. 
    In early 2020, I started a game project using this base but shelved the project due to the fact I was not happy with getting 150fps on a simple scene. This was also pushed when I threw in a fast VR implantation and thought it was really cool (But really slow!). I also was getting random crashes with an actor over something so something was still up with my design. The new engine was coming a long so I re-structured my base yet again. 
    As of today, after many years of work and research I now have a very clean framework that works on both Leadwerks AND the new engine. I see this as more important than ever now as I expect a lot of people are gonna come in for the new engine and be in the same position I was in 2014. 
    At minimum, this is how your main function should look like and will compile with both engines!
    #include "pch.h" #include "gamewindow.h" #include "stage.h" int main(int argc, const char* argv[]) { // Create Window App auto window = CreateGameWindow("FPSGame"); // Create a stage. auto stage = CreateStage(window); stage->LoadSceneFile("Maps/start.map"); // App Loop. while (window->Closed() == false) { stage->Update(); } } This code will create a window, load a map/scene file and update accordingly. Not only for my games, but this also works really well with my current "non-game" project! 
    I wish to go into how into this deeper at a later date, but I thought I should bring context to the last 5 years. Although I didn't get to build a game within that time, I had a blast figuring all this out. 
    I'll leave off with screen shots. Remember. it's same code base doing the same thing for each engine!
    Leadwerks 4:

    New Engine:

     
  13. reepblue
    For some reason, I've been seeing a lot of questions on how to add actors created into C++ recently. My first attempt on this was in 2016 with Crawler's Den; A modern remake of the SDK example level. That was on a version of Leadwerks in which there was no official Actor system in place. Today, the engine has an Actor class which can be attached to any entity, but besides some loose examples, it's not well documented. Also there is no official way on linking your entities in the editor with your actors. But today I wish to share with you some insight on how I managed to get not only actors attaching in my maps, but also allowing interaction with the flowgraph system.
    Disclaimer: This is more copy and paste code only tested with Leadwerks 4.
    Base Actor And Actor Factory
    First, you'll want to make a base class off the engine's Actor class that'll allow us to easily grab values from the lua file. Yes, we can have an Actor and a Script attached to the same entity at a time! The idea is that the lua script will be our definition file for our actor to load on map load.
    #ifndef BASEACTOR_H #define BASEACTOR_H #if defined( _WIN32 ) #pragma once #endif #include "stdafx.h" #define ACTOR_KEYVALUE "classname" class BaseActor : public Actor { public: void SetBoolValue(const std::string& pValue, const bool pDefault = false); bool GetBoolValue(const std::string& pValue, const bool pDefault = false); void SetFloatValue(const std::string& pValue, const float pDefault = 0); float GetFloatValue(const std::string& pValue, const float pDefault = 0); void SetIntValue(const std::string& pValue, const int pDefault = 0); int GetIntValue(const std::string& pValue, const int pDefault = 0); void SetStringValue(const std::string& pValue, const std::string& pDefault = ""); std::string GetStringValue(const std::string& pValue, const std::string& pDefault = ""); Vec2* GetVec2Value(const std::string& pValue, Vec2* pDefault = 0); Vec3* GetVec3Value(const std::string& pValue, Vec3* pDefault = 0); Vec4* GetVec4Value(const std::string& pValue, Vec4* pDefault = 0); Entity* GetEntityValue(const std::string& pValue, Entity* pDefault = NULL); void FireOutput(const std::string& pEvent); }; #endif #include "stdafx.h" #include "baseactor.h" //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void BaseActor::SetBoolValue(const std::string& pValue, const bool pDefault) { #ifndef LEADWERKS_5 GetEntity()->SetBool(pValue, pDefault); #else GetEntity()->SetBoolean(pValue, pDefault); #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool BaseActor::GetBoolValue(const std::string& pValue, const bool pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 if (entity->GetBool(pValue) != pDefault) { return entity->GetBool(pValue); } #else if (GetEntity()->GetBoolean(pValue) != pDefault) { return GetEntity()->GetBoolean(pValue); } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void BaseActor::SetFloatValue(const std::string& pValue, const float pDefault) { #ifndef LEADWERKS_5 GetEntity()->SetFloat(pValue, pDefault); #else GetEntity()->SetNumber(pValue, (float)pDefault); #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- float BaseActor::GetFloatValue(const std::string& pValue, const float pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 if (entity->GetFloat(pValue) != NULL) { return entity->GetFloat(pValue); } #else if (GetEntity()->GetNumber(pValue) != NULL) { return (float)GetEntity()->GetNumber(pValue); } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void BaseActor::SetIntValue(const std::string& pValue, const int pDefault) { GetEntity()->SetString(pValue, to_string(pDefault)); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int BaseActor::GetIntValue(const std::string& pValue, const int pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 if (entity->GetFloat(pValue) != NULL) { return static_cast<int>(entity->GetFloat(pValue)); } #else if (GetEntity()->GetNumber(pValue) != NULL) { double x = GetEntity()->GetNumber(pValue); // stored as 54.999999... x = x + 0.5 - (x < 0); // x is now 55.499999... int y = (int)x; // truncated return y; } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void BaseActor::SetStringValue(const std::string& pValue, const std::string& pDefault) { GetEntity()->SetString(pValue, pDefault); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- std::string BaseActor::GetStringValue(const std::string& pValue, const std::string& pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 if (entity->GetString(pValue) != "") { return entity->GetString(pValue); } #else if (GetEntity()->GetString(pValue) != "") { return GetEntity()->GetString(pValue); } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- Vec2* BaseActor::GetVec2Value(const std::string& pValue, Vec2* pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 Vec2* test = static_cast<Vec2*>(entity->GetObject(pValue)); if (test != NULL) { return test; } #else Vec2* test = (Vec2*)GetEntity()->GetObjectPointer(pValue); if (test != NULL) { return test; } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- Vec3* BaseActor::GetVec3Value(const std::string& pValue, Vec3* pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 Vec3* test = static_cast<Vec3*>(entity->GetObject(pValue)); if (test != NULL) { return test; } #else Vec3* test = (Vec3*)GetEntity()->GetObjectPointer(pValue); if (test != NULL) { return test; } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- Vec4* BaseActor::GetVec4Value(const std::string& pValue, Vec4* pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 Vec4* test = static_cast<Vec4*>(entity->GetObject(pValue)); if (test != NULL) { return test; } #else Vec4* test = (Vec4*)GetEntity()->GetObjectPointer(pValue); if (test != NULL) { return test; } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- Entity* BaseActor::GetEntityValue(const std::string& pValue, Entity* pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 Entity* test = static_cast<Entity*>(entity->GetObject(pValue)); if (test != NULL) { return test; } #else Entity* test = (Entity*)GetEntity()->GetObjectPointer(pValue); if (test != NULL) { return test; } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void BaseActor::FireOutput(const std::string& pEvent) { if (GetEntity() == NULL) return; #ifndef LEADWERKS_5 if (entity->component != NULL) { entity->component->CallOutputs(pEvent); } #else GetEntity()->FireOutputs(pEvent); #endif } Now we need a actor factory of some kind. There is probably an easier and modern way of doing this, but this is how I do it. First we create a header file called actorfactory.h
    #ifndef ACTORFACTORY_H #define ACTORFACTORY_H #if defined( _WIN32 ) #pragma once #endif #include "stdafx.h" extern void BaseActorFactory(Entity* pEntity, Object* pObject); #endif // ACTORFACTORY_H Then in actorfactory.cpp, I have the following. 
    #include "stdafx.h" #include "baseweapon.h" #include "clientcamera.h" #include "doors.h" #include "noise.h" #include "fpsplayer.h" #include "logicactors.h" #include "pointmessage.h" #include "propspawner.h" #include "point_transition.h" #include "platform.h" #include "triggers.h" #include "vrplayer.h" #define ATTACH_NAME_TO_ACTOR(_name_, _actor_) if (test == _name_) actor = new _actor_() void BaseActorFactory(Entity * pEntity, Object * pObject) { auto classname = GetEntityKeyValue(pEntity, ACTOR_KEYVALUE); if (classname != "") { BaseActor* actor = NULL; std::string entname = pEntity->GetKeyValue("name", "STATIC_MESH"); std::string test = String::Lower(classname); // Global actor is a dummy actor with values for other actors. if (test == "global") { if (g_mGlobalActor == NULL) { actor = new BaseActor(); g_mGlobalActor = actor; } } ATTACH_NAME_TO_ACTOR("ambient_generic", AmbientGeneric); ATTACH_NAME_TO_ACTOR("door_sliding", SlidingDoor); ATTACH_NAME_TO_ACTOR("door_rotating", RotatingDoor); ATTACH_NAME_TO_ACTOR("point_path", PointPath); ATTACH_NAME_TO_ACTOR("point_message", PointMessage); ATTACH_NAME_TO_ACTOR("point_transition", PointTransition); ATTACH_NAME_TO_ACTOR("point_weapon_pickup", PointWeaponPickup); ATTACH_NAME_TO_ACTOR("prop_spawner", PropSpawner); ATTACH_NAME_TO_ACTOR("logic_relay", LogicRelay); ATTACH_NAME_TO_ACTOR("logic_branch", LogicBranch); ATTACH_NAME_TO_ACTOR("logic_counter", LogicCounter); ATTACH_NAME_TO_ACTOR("train", TrainActor); ATTACH_NAME_TO_ACTOR("trigger_once", CollisionTrigger); ATTACH_NAME_TO_ACTOR("trigger_multiple", CollisionTriggerMultiple); ATTACH_NAME_TO_ACTOR("volume", BaseVolume); ATTACH_NAME_TO_ACTOR("volume_push", PushVolume); ATTACH_NAME_TO_ACTOR("volume_hurt", HurtVolume); ATTACH_NAME_TO_ACTOR("weapon_pickup", WeaponPickup); if (BaseActor::AttachActor(pEntity, actor, true)) { pEntity->SetKeyValue(ACTOR_KEYVALUE, classname); System::Print("Attached actor: \"" + classname + "\" to \"" + entname + "\"."); } else { System::Print("Error: failed to attach actor: \"" + classname + "\" to \"" + entname + "\"."); } } } Each actor gets a "classname" assigned to it. This can be anything you want and it's defined here. I went with a quake naming scheme for personal preference.
    We then call this function in our map load hook as so. This will pass each entity loaded though our factory when the map file is being loaded in.
    if (Map::Load(pszPath, &BaseActorFactory) == false) { Msg("Failed to load \"" + pszPath + "\" as path is invaild/missing from disk!"); return false; }  
    Custom Actors
    Now we have the platform to build our actor, lets do so! Here's sample code of a relay actor. I'm picking this class as it shows an input and output functions being used that'll work with the flowgraph as well as loading values.
    #ifndef LOGICACTORS_H #define LOGICACTORS_H #if defined( _WIN32 ) #pragma once #endif #include "stdafx.h" #include "baseactor.h" class LogicRelay : public BaseActor { long m_intDelayTime; long m_intTriggerTime; public: virtual void Start(); virtual void ReceiveSignal(const std::string& inputname, Entity* sender); virtual void UpdateWorld(); }; #endif #include "stdafx.h" #include "logicactors.h" //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void LogicRelay::Start() { BaseToggle::Start(); m_intDelayTime = GetIntValue("delay", 0); //FireOutput("OnStart"); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void LogicRelay::ReceiveSignal(const std::string& inputname, Entity* sender) { auto _event = String::Lower(inputname); if (_event == "trigger") { m_intTriggerTime = Timing::GetCurrent(); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void LogicRelay::UpdateWorld() { if (m_intTriggerTime > 0) { if (Timing::GetCurrent() > m_intTriggerTime + m_intDelayTime) { FireOutput("OnTrigger"); } } } Last we write the lua script:
    --[[ Purpose: A Logic entity that is used for relaying outputs. ]]-- Script.classname="logic_relay" Script.delay=-0--int "Delay" function Script:Trigger()--in end function Script:Outputs() self.component:CallOutputs("OnStart") self.component:CallOutputs("OnTrigger") end Compile your C++ code and attach your script to your entity. If all is well, you should have a working actor in your game! 
     
    Remarks
    This is the best and simple way of getting actors to work in your game much like the lua alternative. However, as a C++ user I can ensure you that your adventure isn't over. First you're going to have to figure out how actor members are deleted and when they should be deleted. The clear function in the world class will release all engine actors, but not assets so you need to take that into consideration. Also, I personally ran into issues with sound Source classes (Now called Speaker in Leadwerks 5) not being recreated when the map was reloaded. I ended up having to make my own source management system. To be honest, I forgot how it works since I haven't really touched my project in months. 
    Even after all my workarounds, I still have things not releasing properly sometimes, but if you're smarter than me, I'm sure you can figure it out.
     
    But I wrote this blog so if anyone had questions on how to use actors, You or I can point them here. Happy coding! 
  14. reepblue
    Before we were all locked in our homes, I was working on a lot of code for Leadwerks 4 for a reboot of concept I've made back in 2017. While I decided to shelf it again to wait for the new engine (Because the game will run faster, look better, VR, etc) I'm going to share you some of my core code which is cross compatible between the new and old engine. I'm sharing this as I see most people just using stock example code for window creation and setting saving without knowing how it all works. This is more of a show than tell kind of article, and most things are just copy and paste what's in my code at the moment, but it should give you an idea. In this article, I'll be showing you code for both Leadwerks 4 and 5, but most of this was written and tested in Leadwerks 4. 
     
    Parsing Arguments.
    Leadwerks 4 has a simple command that does this automatically. This makes it so you can give the app flags on how it should startup/behave on startup. 
    System::ParseCommandLine(argc, argv); In Leadwerks 5, you need to store it in your own std::map.
    auto commandline = ParseCommandLine(argc, argv); To be honest, the new way is much better. You see, in Leadwerks 4, System::ParseCommandLine() stored the values in a engine defined std::map which is also associated with System::LoadSettings() and System::SaveSettings(). This will cause unwanted results as flags you may only want to happen only when called in a batch file will save to the cfg file saved by default to the AppData folder in Windows. There is a separate map for both Arguments and Settings, but I think the issue is with if you use System::Set/GetProperty().
    The best option is to keep your arguments and setting separate. In Leadwerks 5, your std::map is yours, so you're already off to a good start.
     
    Actual Settings
    Now we can take in flags/arguments from launch settings, we now need values to be written on the disk. First we need to build the directory in which will act as our data folder for the user. Where this folder should be is up to you, but it should be in the user's directory on Windows, or the user's home directory in Linux. Here's my code for both Leadwerks 4 and 5. 
    First, we need to know the applications name. We use this name value in multiple areas. Leadwerks 4 as a value System::AppName, but in Leadwerks 5, we'll be creating our own variable. We'll also define the settings directory string path here too.
    #ifndef LEADWERKS_5 //std::string AppName = "MyGame"; std::string settingsdir = ""; //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void App::SetName(const std::string& pszAppName) { System::AppName = pszAppName; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- std::string App::GetName() { return System::AppName; } #else std::wstring AppName = L"MyGame"; std::wstring settingsdir = L""; //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void App::SetName(std::wstring pszAppName) { AppName = pszAppName; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- std::wstring App::GetName() { return AppName; } #endif  
    Now we have a name variable, we can use it to build our settings directory. 
    //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void BuildSettingsDirectory() { #ifndef LEADWERKS_5 // If settingsdir is not "", we did this already, no need to do it again. if (settingsdir != "") return; #if defined (_WIN32) auto t = FileSystem::GetDocumentsPath() + "/" + App::GetName(); #else auto t = FileSystem::GetAppDataPath() + "/." + String::Lower(App::GetName()); #endif if (FileSystem::GetFileType(t) == 0) { System::Print("Building Settings folder under:" + t); FileSystem::CreateDir(t); } settingsdir = t; #else // If settingsdir is not "", we did this already, no need to do it again. if (settingsdir != L"") return; #if defined (_WIN32) auto t = FolderLocation(FOLDER_DOCUMENTS) + L"/" + AppName; #else auto t = FolderLocation(FOLDER_APPDATA) + L"/." + Lower(AppName); #endif if (FileType(t) == 0) { Print(L"Building Settings folder under:" + t); CreateDir(t); } settingsdir = t; #endif }  
    You'll now have the path to your settings directory stored in settingsdir. You can use this string to store files there easily like below: (Raw copy and paste, ignore my macros)
    //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void App::TakeScreenShot() { auto framebuffer = Framebuffer::GetCurrent(); if (framebuffer != NULL) { auto screenshot_path = GetSettingsPath() + "/Screenshots"; if (FileSystem::GetFileType(screenshot_path) == 0) { Msg("Building Screenshots folder under:" + screenshot_path); FileSystem::CreateDir(screenshot_path); } framebuffer->Screenshot(screenshot_path + "/" + App::GetName() + "_" + Timing::GetCurrentDate() + ".tga"); } else { Msg("Error: Failed to take screenshot."); } }  
    A long time ago, @Crazycarpet gave me code to parse a text file like a Leadwerks 4 config file or a Leadwerks 4 mat file. I turned that functionality into a class, and instead of having the engine phrase the information, I do this myself. This is a direct copy and paste of my scopes to give you an idea.
    // To Load auto settingspath = settingsdir + "/settings.cfg"; if (FileSystem::GetFileType(settingspath) == 0) { appsettings.fullscreen = false; appsettings.screenwidth = 1280; appsettings.screenheight = 720; appsettings.display = 0; appsettings.antialias = 1; //2 appsettings.lightquality = 1; appsettings.terrainquality = 1; appsettings.texturequality = Texture::GetDetail(); appsettings.waterquality = 1; appsettings.afilter = Texture::GetAnisotropy(); appsettings.verticalsync = false; appsettings.hdrmode = true; } else { auto config = LoadConfig(settingspath); appsettings.fullscreen = config->GetBoolValue("fullscreen", false); appsettings.screenwidth = config->GetIntValue("screenwidth", 800); appsettings.screenheight = config->GetIntValue("screenheight", 600); appsettings.display = config->GetIntValue("display", 0); appsettings.antialias = config->GetIntValue("antialias", 1); appsettings.lightquality = config->GetIntValue("lightquality", 1); appsettings.terrainquality = config->GetIntValue("terrainquality", 1); appsettings.texturequality = config->GetIntValue("texturequality", Texture::GetDetail()); appsettings.waterquality = config->GetIntValue("waterquality", 1); appsettings.afilter = config->GetIntValue("afilter", Texture::GetAnisotropy()); appsettings.verticalsync = config->GetBoolValue("verticalsync", false); appsettings.hdrmode = config->GetBoolValue("hdrmode", true); config = NULL; } //---------------------// // To Save auto config = CreateConfig(settingspath); config->WriteKeyValue("fullscreen", to_string(appsettings.fullscreen)); config->WriteKeyValue("screenwidth", to_string(appsettings.screenwidth)); config->WriteKeyValue("screenheight", to_string(appsettings.screenheight)); config->WriteKeyValue("display", to_string(appsettings.display)); config->WriteKeyValue("antialias", to_string(appsettings.antialias)); config->WriteKeyValue("lightquality", to_string(appsettings.lightquality)); config->WriteKeyValue("terrainquality", to_string(appsettings.terrainquality)); config->WriteKeyValue("texturequality", to_string(appsettings.texturequality)); config->WriteKeyValue("waterquality", to_string(appsettings.waterquality)); config->WriteKeyValue("afilter", to_string(appsettings.afilter)); config->WriteKeyValue("tfilter", to_string(appsettings.tfilter)); config->WriteKeyValue("verticalsync", to_string(appsettings.verticalsync)); config->WriteKeyValue("hdrmode", to_string(appsettings.hdrmode)); bool b = config->WriteOut(); config = NULL; return b;  
    Settings vs Arguments
    Since both settings and arguments are different tables, we must have a way to check if ether the setting or argument is enabled. The best way I chose to do this is to make bool functions that check if ether is true.
    //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool App::IsFullScreen() { if (appsettings.fullscreen == true || GetArgument("fullscreen") == "1") return true; return appsettings.fullscreen; }  
    Building The Window (Finally)
    Now it's time to actually build the window with the information we have via settings file, or augments passed in a command line. 
    For Leadwerks 4, I do something like this:
    Leadwerks::Window* CreateEngineWindow(std::string pszTileName, Leadwerks::Window* parent) { // Are we fullscreen? const bool fullscreenmode = App::IsFullScreen(); // Set the name of the app the same as the window. App::SetName(pszTileName); // Load Settings. App::LoadSettings(); //Load any zip files in main directory Leadwerks::Directory* dir = Leadwerks::FileSystem::LoadDir("."); 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 == "zip" || ext == "pak") { Leadwerks::Package::Load(file, KEY); } } delete dir; } // Create a window Leadwerks::Window* window; std::string windowtitle = App::GetName(); if (IsDebug()) windowtitle = windowtitle + " [Debug]"; if (fullscreenmode) { iVec2 gfx = System::GetGraphicsMode(Leadwerks::System::CountGraphicsModes() - 1); window = Leadwerks::Window::Create(windowtitle, 0, 0, gfx.x, gfx.y, Leadwerks::Window::Fullscreen, parent); } else { window = Leadwerks::Window::Create(windowtitle, 0, 0, App::GetScreenWidth(), App::GetScreenHeight(), Leadwerks::Window::Titlebar | Leadwerks::Window::Center, parent); } EngineWindow::current = window; window->Activate(); return window; }  
    This is my current Leadwerks 5 code:
    shared_ptr<Window> CreateEngineWindow(std::wstring pszTileName, shared_ptr<Window> parent) { //Get the primary display auto display = App::GetDisplay(); // Are we fullscreen? const bool fullscreenmode = App::IsFullScreen(); // Set the name of the app the same as the window. App::SetName(pszTileName); // Load Settings. App::LoadSettings(); // Create a window shared_ptr<Window> window; std::wstring windowtitle = App::GetName(); if (IsDebug()) windowtitle = windowtitle + L" [Debug]"; if (fullscreenmode) { auto gfxmodes = display->GraphicsModes(); auto gfx = gfxmodes[gfxmodes.size() - 1]; window = CreateWindow(display, App::GetName(), 0, 0, gfx.x, gfx.y, WINDOW_FULLSCREEN, parent); } else { Vec2 displayscale = display->GetScale(); window = CreateWindow(display, windowtitle, 0, 0, App::GetScreenWidth() * displayscale.x, App::GetScreenHeight() * displayscale.y, WINDOW_TITLEBAR | WINDOW_CENTER, parent); } return window; }  
    As you might have picked up, I store all of the core app functionality in a singleton class called "App". This is what the header looks like to give you an idea.
    class App { public: #ifndef LEADWERKS_5 static void SetName(const std::string& pszAppName); static std::string GetName(); #else static void SetName(std::wstring pszAppName); static std::wstring GetName(); #endif static void ParseCommandLine(int argc, const char* argv[]); static bool CheckArgument(std::string pszKey); static std::string GetArgument(std::string pszKey); static int GetArgumentInt(std::string pszKey); static float GetArgumentFloat(std::string pszKey); std::map<std::string, std::string> Arguments(); static bool LoadSettings(); static bool SaveSettings(); static AppSettings GetSettings(); #ifndef LEADWERKS_5 static std::string GetSettingsPath(); #else static std::wstring GetSettingsPath(); #endif static void Shutdown(); static bool Running(); static void EnableVR(); static bool IsDevMode(); static bool IsFullScreen(); static unsigned int GetScreenWidth(); static unsigned int GetScreenHeight(); static unsigned int GetDisplayIndex(); #ifdef LEADWERKS_5 static shared_ptr<Display> GetDisplay(); #endif static unsigned int GetAntialias(); static unsigned int GetLightingQuality(); static unsigned int GetTerrainQuality(); static unsigned int GetTextureQuality(); static unsigned int GetWaterQuality(); static unsigned int GetAnisotropicFilter(); static bool GetTrilinearFilter(); static bool GetVerticalSync(); static bool GetHDRMode(); static void TakeScreenShot(); };  
    Bonus: Load JSON Files Easily
    Leadwerks 5 includes the json library included, but as of time of writing, there is no simple way to load the format into something usable. Josh shared his approach using the engine's Stream Class. The json library can also be included in Leadwerks 4 with little to no effort! This is a very nice format to have in your base app code.
    #if defined (EOF) #undef EOF #endif nlohmann::json ReadJSON(const std::string& pszFileName) { #ifndef LEADWERKS_5 auto stream = FileSystem::ReadFile(pszFileName); auto path = FileSystem::RealPath(pszFileName); if (stream == NULL) { Print("Error: Failed to load JSON script \"" + path + "\""); //stream->path = L""; return NULL; } //Check for starting bracket bool started = false; while (!stream->EOF()) { //auto c = stream->ReadLine(); auto c = String::Chr(stream->ReadUChar()); if (String::Trim(c) == "") continue; if (c != "{") return false; started = true; break; } if (!started) return false; stream->Seek(0); #else auto stream = ReadFile(pszFileName); auto path = RealPath(pszFileName); if (stream == NULL) { Print(L"Error: Failed to load JSON script \"" + path + L"\""); //stream->path = L""; return NULL; } //Check for starting bracket bool started = false; while (!stream->Ended()) { auto c = Chr(stream->ReadByte()); if (Trim(c) == "") continue; if (c != "{") return NULL; started = true; break; } if (!started) return false; stream->Seek(0); #endif std::vector<uint8_t> data; auto sz = stream->GetSize(); data.resize(sz); stream->Read(data.data(), sz); nlohmann::json j3; try { j3 = nlohmann::json::parse(data); } catch (std::exception & e) { std::cout << e.what() << std::endl; return false; } if (j3.type() != nlohmann::json::value_t::object) return NULL; #ifndef LEADWERKS_5 stream->Release(); #endif stream = NULL; return j3; } #ifdef LEADWERKS_5 nlohmann::json WReadJSON(const std::wstring& pszFileName) { auto stream = ReadFile(pszFileName); auto path = RealPath(pszFileName); if (stream == NULL) { Print(L"Error: Failed to load JSON script \"" + path + L"\""); //stream->path = L""; return NULL; } //Check for starting bracket bool started = false; while (!stream->Ended()) { auto c = Chr(stream->ReadByte()); if (Trim(c) == "") continue; if (c != "{") return NULL; started = true; break; } if (!started) return false; stream->Seek(0); std::vector<uint8_t> data; auto sz = stream->GetSize(); data.resize(sz); stream->Read(data.data(), sz); nlohmann::json j3; try { j3 = nlohmann::json::parse(data); } catch (std::exception & e) { std::cout << e.what() << std::endl; return false; } if (j3.type() != nlohmann::json::value_t::object) return NULL; stream = NULL; return j3; } #endif  
    This type of code are things you can do right now in the Leadwerks 5 beta while you wait for the rest of the engine to come online. As for me, I already have this code done and this layer of stuff works flawlessly and produces the same results on both engines. If there is anything else you want me to share based on core.
  15. reepblue
    Premake is multiplication project maker.Unlike CMake, it simply generates a project file for the given IDE giving you a clean result. You only need the one light weight executable and a lua script for this to work.  I've spent today setting it up with Leadwerks. I haven't tested Linux yet, but it should work.
    My premake5.lua file:
    g_LeadwerksHeaderPath = "./Engine/Include" g_LeadwerksLibPath = "./Engine/Libs" function GlobalSettings() -- Include Directories includedirs { "%{prj.name}", "%{g_LeadwerksHeaderPath}", "%{g_LeadwerksHeaderPath}/Libraries/SDL2-2.0.10/include", "%{g_LeadwerksHeaderPath}/Libraries/NewtonDynamics/sdk/dgCore", "%{g_LeadwerksHeaderPath}/Libraries/NewtonDynamics/sdk/dgNewton", "%{g_LeadwerksHeaderPath}/Libraries/libvorbis/include", "%{g_LeadwerksHeaderPath}/Libraries/libogg/include", "%{g_LeadwerksHeaderPath}/Libraries/openssl/include", "%{g_LeadwerksHeaderPath}/Libraries/VHACD/src/VHACD_Lib/inc", "%{g_LeadwerksHeaderPath}/Libraries/glslang", "%{g_LeadwerksHeaderPath}/Libraries/freetype-2.4.7/include", "%{g_LeadwerksHeaderPath}/Libraries/OpenAL/include", "%{g_LeadwerksHeaderPath}/Libraries/NewtonDynamics/sdk/dMath", "%{g_LeadwerksHeaderPath}/Libraries/NewtonDynamics/sdk/dgTimeTracker", "%{g_LeadwerksHeaderPath}/Libraries/NewtonDynamics/sdk/dContainers", "%{g_LeadwerksHeaderPath}/Libraries/NewtonDynamics/sdk/dCustomJoints", "%{g_LeadwerksHeaderPath}/Libraries/RecastNavigation/RecastDemo/Include", "%{g_LeadwerksHeaderPath}/Libraries/RecastNavigation/DetourCrowd/Include", "%{g_LeadwerksHeaderPath}/Libraries/RecastNavigation/DetourTileCache/Include", "%{g_LeadwerksHeaderPath}/Libraries/RecastNavigation/DebugUtils/Include", "%{g_LeadwerksHeaderPath}/Libraries/RecastNavigation/Recast/Include", "%{g_LeadwerksHeaderPath}/Libraries/RecastNavigation/Detour/Include", "%{g_LeadwerksHeaderPath}/Libraries/tolua++-1.0.93/include", "%{g_LeadwerksHeaderPath}/Libraries/lua-5.1.4", "%{g_LeadwerksHeaderPath}/Libraries/glew-1.6.0/include/GL", "%{g_LeadwerksHeaderPath}/Libraries/glew-1.6.0/include", "%{g_LeadwerksHeaderPath}/Libraries/enet-1.3.1/include", "%{g_LeadwerksHeaderPath}/Libraries/zlib-1.2.5", "%{g_LeadwerksHeaderPath}/Libraries/freetype-2.4.3/include" } -- Global Defines: defines { "__STEAM__", "_CUSTOM_JOINTS_STATIC_LIB", "FT2_BUILD_LIBRARY", "LEADWERKS_3_1", "DG_DISABLE_ASSERT", "OPENGL", "_NEWTON_STATIC_LIB", "_STATICLIB" } -- Windows Exclusive: filter "system:windows" systemversion "latest" pchsource "%{prj.name}/stdafx.cpp" links { "libcryptoMT.lib", "libsslMT.lib", "Rpcrt4.lib", "crypt32.lib", "libcurl.lib", "msimg32.lib", "lua51.lib", "steam_api.lib", "ws2_32.lib", "Glu32.lib", "libovrd.lib", "OpenGL32.lib", "winmm.lib", "Psapi.lib", "OpenAL32.lib", "SDL2.lib", "Leadwerks.lib" } libdirs { "%{g_LeadwerksLibPath}/Windows/x86", "%{g_LeadwerksLibPath}/Windows/x86/%{cfg.buildcfg}" } defines { "PSAPI_VERSION=1", "PTW32_STATIC_LIB", "PTW32_BUILD", "_NEWTON_USE_LIB", "_LIB", "DG_USE_NORMAL_PRIORITY_THREAD", "GLEW_STATIC", "WINDOWS", "WIN32", "OS_WINDOWS", "PLATFORM_WINDOWS", "_WIN_32_VER" } buildoptions { "/D \"SLB_LIBRARY\"", } flags { "NoMinimalRebuild" } linkoptions { "/NODEFAULTLIB:MSVCRT.lib", "/NODEFAULTLIB:MSVCRTD.lib" } -- Linux Exclusive: filter "system:linux" systemversion "latest" linkoptions { "-ldl", "-lopenal", "-lGL", "-lGLU", "-lX11", "-lXext", "-lXrender", "-lXft", "-lpthread", "-lcurl", --"-lSDL2", "%{g_LeadwerksLibPath}/Linux/libluajit.a", "%{gameDir}/libopenvr_api.so" } defines { "ZLIB", "PLATFORM_LINUX", "unix", "_POSIX_VER", "_POSIX_VER_64", "DG_THREAD_EMULATION", "DG_USE_THREAD_EMULATION", "GL_GLEXT_PROTOTYPES", "LUA_USE_LINUX", "_GLIBCXX_USE_CXX11_ABI", "_CUSTOM_JOINTS_STATIC_LIB" } linkoptions { "%{g_LeadwerksLibPath}/Linux/%{cfg.buildcfg}/Leadwerks.a" } -- Debug Build: filter "configurations:Debug" runtime "Debug" symbols "on" targetsuffix ".debug" defines { "DEBUG", "_DEBUG" } if os.target() == "windows" then links { "newton_d.lib", "dContainers_d.lib", "dCustomJoints_d.lib" } end -- Release Build: filter "configurations:Release" runtime "Release" optimize "on" if os.target() == "windows" then buildoptions { "/MP" } links { "newton.lib", "dContainers.lib", "dCustomJoints.lib" } end end function GenerateLuaApp() workspace "PremakeTest" architecture "x86" --architecture "x86_64" startproject "LuaApp" configurations { "Debug", "Release" } -- Test application project "LuaApp" kind "ConsoleApp" language "C++" location "%{prj.name}" staticruntime "on" -- Project Directory: projectDir = "%{prj.name}/" -- Game Directory: gameDir = _WORKING_DIR .. "/../Projects/%{prj.name}" -- OBJ Directory objdir (projectDir .. "%{cfg.buildcfg}_%{prj.name}") targetdir (gameDir) files { "%{prj.name}/**.h", "%{prj.name}/**.cpp" } pchheader "stdafx.h" -- Custom Defines defines { "__TEST_ME_", } GlobalSettings() end newaction { trigger = "luaapp", description = "Builds the stock lua app", execute = GenerateLuaApp() } if _ACTION == "luaapp" then GenerateLuaApp() end Then I just have batch file that builds the project file.
    "devtools/premake5.exe" vs2017 luaapp pause My impressions are more positive on this than CMake as CMake creates a lot of extra files to do real time updating if the CMakeList file is updated. Premake simply just builds the project file and that's it. It really reminds me of VPC stuff I had to deal with in my modding days. Really interested to how codelite projects generate on Linux.
  16. reepblue
    I think I've finally finished my approach in input for my project. This is the final result.
    //========= Copyright Reep Softworks, All rights reserved. ============// // // Purpose: // //=====================================================================// #include "stdafx.h" #include "gamewindow.h" #include "gameworld.h" int main(int argc, const char* argv[]) { App::ParseCommandLine(argc, argv); auto window = CreateGameWindow("GameTemplate"); auto world = CreateGameWorld(window); if (App::GetArgument("vr") != "") App::EnableVR(); auto camera = Camera::Create(); camera->SetPosition(0, 0, -1); ActionConfig::SetActiveSet("Walking"); while (App::Running()) { if (GetActionHit("jump")) { DMsg("jump!"); } if (GetActionHit("interact")) { DMsg("interact!"); } world->Render(); } return 0; } We no longer care what key or button is pressed, we are now concerned what action is being sent to us.Two json files are auto generated in "CreateGameWindow()". One for the mouse and keyboard, and the other for a gamepad with SDL2. These files can be modified by the user (hard) or an app when I actually write it (easier).
    { "keyBindings": { "actionStates": { "Menu": { "pause": 27, "selectActive": 13, "selectDown": 40, "selectLeft": 37, "selectRight": 39, "selectUp": 38 }, "Walking": { "crouch": 17, "firePrimary": 1, "fireSecondary": 2, "flashLight": 70, "interact": 69, "jump": 32, "moveBackward": 83, "moveForward": 87, "moveLeft": 65, "moveRight": 68, "pause": 27, "reloadWeapon": 82 } } } } { "joyBindings": { "actionStates": { "Menu": { "joySelect": 0, "pause": 6, "selectActive": 0, "selectDown": 12, "selectLeft": 13, "selectRight": 14, "selectUp": 11 }, "Walking": { "crouch": 7, "firePrimary": 16, "fireSecondary": 15, "flashLight": 3, "interact": 2, "joyLook": 1, "joyMovement": 0, "jump": 0, "pause": 6, "reloadWeapon": 1 } } } } You may notice that "jump" and "joyMovement" are both 0, but they get treated differently. I've added a GetActionValue() function which returns a Vec2 for analog sticks and triggers. Triggers are both read as buttons and analog values. You still have to write code in how you want to treat the sticks, but the question of the left stick or right stick is bindable now.
    There are also Action Sets a collection of buttons can be set based on what state the game is at. Driving is not the same as walking, nor it's the same as flying a plane.
    VR can also have it's own entry, but I feel you should use SteamInput for that since the Engine is dependent on SteamVR for it's VR implementation. I've could have just used SteamInput at the start, but then my binding code would only work if the game is on Steam.
    My code was compiled on Leadwerks 4.6 beta, but thanks to my wrapper functions and back and forth compiling, it should just work in the new engine.
  17. reepblue
    As you may have known, I've been dabbling with input methods for a while now using SDL2. Since then, I've learned how to do similar functions using the Leadwerks API. The goal was to make a inout system that's easily re-bindable, and allows for controllers to "just work". My first research of a goof system comes from a talk at Steam DevDays 2016 as they discuss how to allow integration with the Steam Controller. 
     
    My thought was: "If I can create my own Action System, I can bind any controller with any API I want". The SDL experiments was a result of this, but they ended up being sloppy when you tried to merge the window polling from SDL into Leadwerks.
    The next goal was to remove SDL2 out of the picture. I've created functions to allow reading and simulations of button presses with the Leadwerks Window class.
    //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool InputSystem::KeyHit(const int keycode) { auto window = GetActiveEngineWindow(); if (keycode < 7) return window->MouseHit(keycode); return window->KeyHit(keycode); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool InputSystem::KeyDown(const int keycode) { auto window = GetActiveEngineWindow(); if (window != NULL) { if (keycode < 7) return window->MouseDown(keycode); return window->KeyDown(keycode); } return false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void InputSystem::SimulateKeyHit(const char keycode) { auto window = GetActiveEngineWindow(); if (window != NULL) { if (keycode < 7) window->mousehitstate[keycode] = true; window->keyhitstate[keycode] = true; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void InputSystem::SimulateKeyDown(const char keycode) { auto window = GetActiveEngineWindow(); if (window != NULL) { if (keycode < 7) window->mousedownstate[keycode] = true; window->keydownstate[keycode] = true; } } The simulate keys are very important for controllers. for this case, we would trick the window class thinking a key was pressed on the keyboard. The only direct input we would need from the controller is the value analog sticks which I haven't touch as of yet.
     Using JSON, we can load and save our bindings in multiple Action Sets!
    { "keyBindings": { "actionStates": { "Menu": { "selectActive": 1, "selectDown": 40, "selectLeft": 37, "selectRight": 39, "selectUp": 38 }, "Walking": { "crouch": 17, "firePrimary": 1, "fireSecondary": 2, "flashLight": 70, "interact": 69, "jump": 32, "moveBackward": 83, "moveForward": 87, "moveLeft": 65, "moveRight": 68, "reloadWeapon": 82 } } } } You may want a key to do something different when your game is in a certain state. For this example, when the Active Action Set is set to "Menu", Only KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, and KEY_LBUTTON will work. You can still hover over your buttons with the mouse, but when it comes time to implement the controller for example, you'd just call GetActionHit(L"selectActive") to select the highlighted/active button. If the state is set to walking, then all those keys for Menu gets ignored in-favor of the walking commands. All keys/buttons are flushed between switching states!
    Here's example code of this working. "Interact" gets ignored when "Menu" is set as the default action and vise-versa.
    while (window->KeyDown(KEY_END) == false and window->Closed() == false) { if (window->KeyHit(KEY_TILDE)) { if (InputSystem::GetActiveActionSet() == L"Menu") { SetActionSet(L"Walking"); } else { SetActionSet(L"Menu"); } } // Under "Menu" if (GetActionHit(L"selectUp")) { DMsg("selectUp!"); } // Under "Walking" if (GetActionHit(L"interact")) { DMsg("interact!"); } } Only things I didn't implement as of yet is actual controller support and saving changes to the json file which might need to be game specific. I also want to wait until the UI is done before I decide how to do this.
    As for controllers, we can use SteamInput, but what if your game isn't on Steam? You can try to implement XInput yourself if you want. I tried to add Controller support with SDL2, but people reported issues. And then, what about VR controllers? What matters right now is that we have room to add these features later on. All I need to do is use the GetActionHit/Down Commands and the rest doesn't matter.
  18. reepblue
    There has been some discussion regarding on how to set collision shapes for your models. For 95% of models, you should be building shapes with the Model Viewer as described here. In some cases, the model artist might want a custom shape to be made. In this post, I'll be going over how I import models into Leadwerks, and building custom shapes.
    A few notes first. I use Blender; Blender 2.79b to be exact. I haven't get got the hang of 2.80 and until the new engine's art pipeline is fully online, I don't see a use for it. Leadwerks 4 uses Blinn-Phong rendering so the PBR stuff makes no sense for Leadwerks 4. So for this, I'll be posting screenshots from 2.79b. I should also mentioned that a feature I use in my process isn't present in the Linux build of the editor, which is the collapse tool. (Tools->Collapse). Doing the collapsing via a terminal will cause the models to crash the editor. This seems to be a known bug, as you don't see that feature in the Linux editor.
    Lets say you created a tube model such as this one and you want the player and objects to go into the tube:

    If you tried to make a shape doing the Concave settings, not only it'll be really slow to generate, but the results will not be good. We could make a shape based on the wire frame, but this is a high poly model. What we need to do is make a new mesh, import both models  to the editor, collapse them both, build the shapes for both, and delete the low poly model while making the high poly read the low poly's generated shape.
     
    First to get it out of the way, apply the scale and rotation of the model. This will make Y forward and the scale will be (1,1,1) when you import it into Leadwerks.

     
    Next we need a low poly model.

    This is the same proportions as our high poly. Apply the scale and rotation as the same as the high poly. I also set the max draw time to solid, but this is optional.

    Next, name your High poly and the low poly you're going to be using for the shape appropriately.

    Now lets, export each object as a FBX. For this my high poly is going out as tube.fbx, and my low poly shape is going out as tubeshape.fbx. Here are my export settings:

    If you saved the files in a Leadwerks project while the editor was opened, the editor would have auto convert the files to the .mdl file format. Open the high poly model (tube.fbx) and first collapse it and give it any shape. (Give it a box shape to save time.) you need to assign a shape to the high poly so the mdl file is linked to a phys file. Do the same with the low poly, but you're gonna set the shape as poly mesh.


    Close the model viewer, and then go into the directory where the models are placed. We are now going to delete the box shape of our high poly, and trick it into loading the low poly shape by renaming the shape file of the low poly to be what the previous shape of the high poly was. In other words, we are making tubeshape.phy into tube.phy.
    Before:

    After:

    Notice the time stamp and the size of tubeshape.phy from before being the same as tube.phy in the after screen cap. This should be your end result.

    Notice that the shape isn't sold but now a tube. Objects can go into the tube with no issues. Now, there is another way that uses limb names to generate physics automatically. However, there are a lot of issues I came across using this method such as the shape not being parented to the model when the model moved via physics or a joint. With this way, you have a custom shape, the model is optimized because it doesn't have any children nodes, and everything is clean and tidy!

     
  19. reepblue
    Loading sounds in Leadwerks has always been straight forward. A sound file is loaded from the disk, and with the Source class emits the sound in 3D space. The sound entity also has a play function, but it's only really good for UI sounds. There is also Entity::EmitSound() which will play the sound at the entity's location. (You can also throw in a Source, but it'll auto release the object when it's done.)
    While this is OK for small games, larger games in which sounds may change might mean you have to open your class, and adjust the sounds accordingly. What if you use the sound in multiple places and you're happy with the volume and pitch settings from an earlier implementation? You could just redefine the source in a different actor, but why should you?
    A solution I came up with comes from SoundScripts from the Source Engine. With that engine, you had to define each sound as a SoundScript entry. This allowed you to define a sound once, and it allowed for other sound settings such as multiple sounds per entry. I thought this over, and with JSON, we can easily create a similar system for Leadwerks 4 and the new engine.
    I first started with a dummy script so I can figure out how I wanted the end result to be.
    { "soundData": { "Error": { "file": "Sound/error.wav", "volume": 1.0, "pitch": 1.0, "range": 0.25 }, "RandomSound": { "files": { "file1": "Sound/Test/tone1.wav", "file2": "Sound/Test/tone2.wav", "file3": "Sound/Test/tone3.wav" }, "volume": 1.0, "pitch": 1.0, "range": 0.25 } } } In this script, we have two sound entries. We have an error sound (Which is suppose to be the fall back sound for an invalid sound entry) and we have a sound entry that holds multiple files. We want a simple, straight forward. entry like "Error" to work, while also supporting something "RandomSound" which can be used for something like footstep sounds.
    The script is streamed and stored into multiple structs in a std::map at the application start. We use the key for the name, and the value is the struct.
    typedef struct { std::string files[128]; char filecount; float volume; float pitch; float range; bool loopmode; } sounddata_t; std::map<std::string, sounddata_t> scriptedsounds; Also notice that we don't store any pointers, just information. To do the next bit, I decided to derive off of the engine's Source class and call it "Speaker". The Speaker class allows us to load sounds via the script entry, and support multiple sounds.
    You create one like this, and you have all the functionalities with the Source as before, but a few differences.
    // Speaker: auto speaker = CreateSpeaker("RandomSound"); When you use Play() with the speaker class and if the sound entry has a "files" table array, it'll pick a sound at random. You can also use PlayIndex() to play the sound entry in the array. I also added a SetSourceEntity() function which will create a pivot, parent to the target entity. From there, the Play function will always play from the pivot's position. This is a good alternative to Entity::EmitSound(), as you don't need to Copy/Instance the Source before calling the function as that function releases the Source as mentioned earlier. Just play the speaker, and you'll be fine! You can also change the sound entry at anytime by calling SetSoundEntry(const std::string pSoundEntryName); The creation of the Speaker class will start the JSON phrasing. If it has already been done, it will not do it again.
    Having sounds being loaded and stored like this opens up a lot of possibles. One thing I plan on implementing is a volume modifier which will adjust the volume based on the games volume setting.Right now, it uses the defined volume setting. It's also a part of another system I have in the works.
  20. reepblue
    While we have Leadwerks today to make our apps, Turbo is currently in development. To make my projects more long term, I decided to take a step back from my VR project and create a wrapper class that apps can use to more easily transfer to the new engine.  Keep in-mind, this doesn't cover everything and I'm used the last beta from November for testing so things can change. Regardless, as long as that bridge is made, I can always go back and edit the Turbo stuff as more things come online. The goal is to make the Leadwerks stuff compatible with the new tech.
    This code works on both engines.
    #include "GameEngine.hpp" using namespace ENGINE_NAMESPACE; int main(int argc, const char *argv[]) { auto window = Create_Window("MyGame"); auto context = Create_Context(window); auto world = Create_World("Maps/testrender.map"); while (window->Closed() == false) { if (window->KeyHit(KEY_ESCAPE)) break; UpdateWorld(); RenderWorld(); #ifndef TURBO_ENGINE context->Sync(true); #endif } return 0; } As you can see, I went with with the cleaner style of the new engine. The only thing is ugly is the context syncing for Leadwerks, I didn't want wrap that since context syncing is totally different between Leadwerks and the new engine.
    Marcos!
    To comply with the new engine's integer values, I've defined a few macros that is only defined in Leadwerks. While this doesn't cover everything right now, So far I got the Window stuff, Collision, and all keys defined!
    #ifndef TURBO_ENGINE // ------------- // Window: // ------------- #define WINDOW_TITLEBAR Window::Titlebar #define WINDOW_RESIZABLE Window::Resizable #define WINDOW_HIDDEN Window::Hidden #define WINDOW_CENTER Window::Center #define WINDOW_FULLSCREEN Window::FullScreen // ------------- // Color: // ------------- #define COLOR_DIFFUSE Leadwerks::Color::Diffuse #define COLOR_SPECULAR Leadwerks::Color::Specular #define COLOR_EDIT Leadwerks::Color::Edit // ------------- // Collision: // ------------- #define COLLISION_NONE Collision::None #define COLLISION_PROP Collision::Prop #define COLLISION_SCENE Collision::Scene #define COLLISION_CHARACTER Collision::Character #define COLLISION_TRIGGER Collision::Trigger #define COLLISION_DEBRIS Collision::Debris #define COLLISION_COLLIDE Collision::Collide // Not Turbo'd #define COLLISION_PROJECTILE Collision::Projectile // Not Turbo'd #define COLLISION_LINEOFSIGHT Collision::LineOfSight // Not Turbo'd #define COLLISION_FLUID Collision::Fluid // Not Turbo'd #define COLLISION_LASTENTRY COLLISION_FLUID // Not Turbo'd  
    Shared Pointers vs Raw
    The new engine uses smart pointers to replace the current reference system. I needed macros to address this to prevent my code being #ifdef/#endif living Hell.
    // Smart Pointers vs Raw Pointers #ifdef TURBO_ENGINE #define ENGINE_OBJECT(x) std::shared_ptr<x> #define ENGINE_OBJECT_RELEASE(x) if (x != nil) { x->Free(); x = nil; } #define ENGINE_REFERENCE(x) std::weak_ptr<x> #define ENGINE_CAST(_class_, _pointer_) dynamic_pointer_cast<_class_>(_pointer_); #else #define ENGINE_OBJECT(x) x* #define ENGINE_OBJECT_RELEASE(x) if (x != nil) { x->Release(); x = nil; } #define ENGINE_REFERENCE(x) x* #define ENGINE_CAST(_class_, _pointer_) (_class_*)_pointer_ #endif To my surprise, you don't delete an entity alone calling something like "model = NULL". This is because the world class keeps a list of all entities in the world via a shared pointer. Instead (for right now) you call the Free command to totally remove the model. Another thing to note is that with Leadwerks, ENGINE_REFERENCE is the same as ENGINE_OBJECT. while ENGINE_REFERENCE is a weak pointer.
     
    Setters and Getters.
    First thing first, I needed to redefine the the Setters/Getters system from Leadwerks. While the decision to remove this functionality opens the door for more possiblites thanks to the new engine's architecture, In my five years of using Leadwerks, I never had a needed more than one world/context/window for most use cases.
    //----------------------------------------------------------------------------- // Purpose: We still need Getters/Setters to comply with LE code. //----------------------------------------------------------------------------- namespace ENGINE_NAMESPACE { void SetCurrentWindow(ENGINE_OBJECT(Window) pWindow); void SetCurrentContext(ENGINE_OBJECT(Context) pContext); void SetCurrentWorld(ENGINE_OBJECT(World) pWorld); void SetCurrentCamera(ENGINE_OBJECT(Camera) pCamera); ENGINE_OBJECT(Window) GetCurrentWindow(); ENGINE_OBJECT(Context) GetCurrentContext(); ENGINE_OBJECT(World) GetCurrentWorld(); ENGINE_OBJECT(Camera) GetCurrentCamera(); } While in Turbo, the object are assigned to pointers in the cpp file, In Leadwerks, this is just a wrapper for the Set/GetCurrent functions.
    Then we have wrapper functions for every create and load function. So we just do this to set our currents.
    // Creates a window. ENGINE_OBJECT(Window) Create_Window(const std::string& pszTitle, const int x, const int y, const int iWidth, const int iHeight, const int iStyle, ENGINE_OBJECT(Window) pParent) { #ifndef TURBO_ENGINE auto window = Window::Create(pszTitle, x, y, iWidth, iHeight, iStyle, pParent); #else auto window = CreateWindow(pszTitle, x, y, iWidth, iHeight, iStyle, pParent); #endif SetCurrentWindow(window); #ifdef WIN32 window->SetActive(); #endif return window; } // Creates a Context. ENGINE_OBJECT(Context) Create_Context(ENGINE_OBJECT(Window) pWindow) { #ifndef TURBO_ENGINE auto context = Context::Create(pWindow); #else auto context = CreateContext(pWindow); #endif SetCurrentContext(context); return context; }  
    World
    How the world loads scenes between the two engines is completely different! In Turbo, the map is loaded to a shared pointer in which you can throw around your application while in Leadwerks it's just a static function call. As the goal is to just make a compatibility layer,  I just had to do something like this:
    // Loads a scene into the current world. bool Load_Scene(const std::string& pszPath, void hook(ENGINE_OBJECT(Entity) entity, ENGINE_OBJECT(Object) extra)) { #ifdef TURBO_ENGINE if (m_pCurrentWorld.lock() == nil) return false; if (FileSystem::GetFileType(pszPath) == 0) { Msg("Error: Failed to load scene file " + ENGINE_NAMESPACE::FileSystem::FixPath(pszPath) + "\"...") return false; } auto scene = Turbo::LoadScene(GetCurrentWorld(), pszPath); // There is no C++ maphooks rn. :( return scene != nil; #else return Map::Load(pszPath, hook); #endif } // Creates a world. ENGINE_OBJECT(World) Create_World(const std::string& pszPath) { #ifdef TURBO_ENGINE auto world = CreateWorld(); #else auto world = World::Create(); #endif SetCurrentWorld(world); if (pszPath != "") { if (Load_Scene(pszPath) == false) { Msg("Error: Failed to load scene \"" + pszPath + "\"..."); } } return world; }  
    Creating Entities
    Entities in Turbo can be assigned to any world pointer upon creation while with Leadwerks, it creates the entity to the world that's set to current. This was a easy fix as again, we are are just doing a compatibility layer. While the style is more Turbo like, there is no world argument, it grabs the current pointer just like Leadwerks.
    // Creates a Pivot. ENGINE_OBJECT(Pivot) Create_Pivot(ENGINE_OBJECT(Entity) pParent) { #ifdef TURBO_ENGINE if (pParent != nil) return CreatePivot(pParent); return CreatePivot(GetCurrentWorld()); #else return Pivot::Create(pParent); #endif } // Creates a Camera. ENGINE_OBJECT(Camera) Create_Camera() { #ifdef TURBO_ENGINE return CreateCamera(GetCurrentWorld()); #else return Camera::Create(); #endif } // Creates a Listener. ENGINE_OBJECT(Listener) Create_Listener(ENGINE_OBJECT(Entity) pParent) { #ifdef TURBO_ENGINE return CreateListener(GetCurrentWorld(), pParent); #else return Listener::Create(pParent); #endif }  
    Lights are different in the new engine. Instead of lights being their own classes, the new "CreateLight" function returns "Light". Again, I solved this with Macros.
    // Creates a Point Light. ENGINE_OBJECT(CLASS_POINTLIGHT) Create_PointLight(ENGINE_OBJECT(Entity) pParent) { #ifdef TURBO_ENGINE return CreateLight(GetCurrentWorld(), LIGHT_POINT, pParent); #else return CLASS_POINTLIGHT::Create(pParent); #endif } // Creates a Spot Light. ENGINE_OBJECT(CLASS_SPOTLIGHT) Create_SpotLight(ENGINE_OBJECT(Entity) pParent) { #ifdef TURBO_ENGINE return CreateLight(GetCurrentWorld(), LIGHT_SPOT, pParent); #else return CLASS_SPOTLIGHT::Create(pParent); #endif } // Creates a Directional Light. ENGINE_OBJECT(CLASS_DIRECTIONALLIGHT) Create_DirectionalLight(ENGINE_OBJECT(Entity) pParent) { #ifdef TURBO_ENGINE return CreateLight(GetCurrentWorld(), LIGHT_DIRECTIONAL, pParent); #else return CLASS_DIRECTIONALLIGHT::Create(pParent); #endif } And loading things are the same concept. . . .
    // Load a Sound file. ENGINE_OBJECT(Sound) Load_Sound(const std::string& pszPath, const int iFlags) { #ifndef TURBO_ENGINE return Sound::Load(pszPath, iFlags); #else return LoadSound(pszPath, iFlags); #endif }  
    That's about it. I'm going to use this going forward updating as I go and hopefully doing this will make the transition less painful when the time comes.
  21. reepblue
    Luawerks has been updated to 1.2.8, making some small adjustments and fixes to the system. If you have previously purchased Luawerks, this update is available for free on the Leadwerks Marketplace.
    Following changes include:
    Fixed GUI code for Leadwerks Game Engine 4.6 Removed the feature "allowfullscreenfromeditor" as it was causing conflicts with fullscreen. Added ignoreeditorwinsettings bool setting to force the game to ignore editor launch settings. (Sorry for the delay..)
    About Luawerks
    Luawerks is a Lua framework for video games developed on the Leadwerks Game Engine. It supplies developers with additional functions, handles the game loop and allows them to debug their code with a developers console. With all that out of the way, you can focus on making your game fun!
    You can purchase Luawerks from the Leadwerks Marketplace for $9.99.
    For documentation and bug reporting, please visit the GitHub page.
  22. reepblue
    The Leadwerks API has standard Boolean functions that detect when the end user has pressed a key. While this is very simple and easy to understand, the issue comes when you wish to support binding of actions. Instead calling functions when a certain key was pressed or held, a better way to detect key events is to assign a key to an action. (e.g: Is the Jump key pressed). In Luawerks, I've written an action script in which returns the window call if the key defined in the user's settings is hit. The downsides is that you had to do this for every action in your game and that this was a system written for Luawerks so it wasn't very portable for non-lua/Leadwerks projects.
    function Action:MoveForward() if self.suspend==true then return false end local key_forward = GetKeyInt(System:GetProperty("key_forward","w")) return window:KeyDown(key_forward) end For those who don't know, SDL is an Open Source library that comes in handy for things like Window management, input, and such. So over the weekend, I've decided to sit down and create my own Input System in which would be allow for action bindings, portable for any type of project while supporting Game Pads outside of Steam.
    The first step was to manually poll all inputs and treat them all the same. For the mouse and keyboard, this was just seeing if a key was hit, and registering that event to a map.
    namespace RInput_KM { void SimulateButton(const Sint32& pKey, bool bDown); const float ButtonDown(const Sint32& pKey); void ShowMouse(); void HideMouse(); void SetMousePosition(const int x, const int y, bool bGlobal = false); void ModMousePosition(const int x, const int y, bool bGlobal = false); void UpdateMousePosition(); void SimulateMouse(const int x, const int y); const int GetMouseX(); const int GetMouseY(); void SetMouseWheelPosition(const int y); const bool OnMouseWheelUp(); const bool OnMouseWheelDown(); // Returns the long to a character. const char* GetButtonName(const Sint32& pButton); // Returns the character to a long const Sint32 GetButtonIndex(const char* pButton); void Enable(); void Disable(); void FlushKeyboard(); void FlushMouse(); void Flush(); } When it comes to buttons, it doesn't matter if the mouse button was pressed or a key stroke. What matters is that a button on the keyboard and mouse combo was pressed. I treat the keyboard and mouse as one controller. You can also "Turn off" the mouse and keyboard if you want.
    namespace RInput_GamePad { typedef enum { ENUM_GAMEPAD_NULL = -1, ENUM_GAMEPAD_ONE, ENUM_GAMEPAD_TWO, ENUM_GAMEPAD_THREE, ENUM_GAMEPAD_FOUR, ENUM_GAMEPAD_MAX = ENUM_GAMEPAD_FOUR } GamePadIndex; typedef struct { SDL_GameController* controller; const char* pszDeviceName; bool bEnabled; std::map<Uint8, bool> mControllerButtons; } gamepad_t; void Connect(const Sint32& pWhich); void Disconnect(const Sint32& pWhich); gamepad_t GetDeviceFromPort(const GamePadIndex& pPort); // Digital input: void SimulateButton(const Sint32& pWhich, const Uint8& pButton, bool bDown); const float ButtonDown(const Uint8& pButton, const GamePadIndex& iIndex = ENUM_GAMEPAD_ONE); const char* GetButtonName(const Uint8& pButton); const Uint8 GetButtonIndex(const char* pButton); // Analog input: void UpdateAxisMotions(const Sint32& pWhich, const Uint32& pAxis); const Sint16 GetAxisValue(const Sint32& pWhich, const Uint32& pAxis, bool bFlip = false); const float GetAxisFloat(const Sint32& pWhich, const Uint32& pAxis, bool bFlip = false); void Flush(const Sint32& pWhich); void FlushAll(); } Next was the game pads. which were a bit more challenging as you need to consider multiple game pads and the valves of analog inputs.
    If you were paying attention, You would notice that the ButtonDown functions for both the keyboard+mouse and the game pad are returning floats. While it may be limiting for certain applications, I've created "fake buttons" for events for when the trigger is pressed, or if the left stick is to the left. All input returns a float ranging from 0.0 to 1.0. For digital inputs like buttons, this will be a 1 or 0, but for analog the value depending the range from the resting point to the max will called. So if the left stick is all the way to the left, you'd get 1; half way 0.5. This is much better then dealing with direct Uint32 values.
    Last was to merge both controllers into one entry point in which we use in our app to register our actions, and check to see if they are being called.
    namespace RInput { typedef enum { CONTROLLER_KEYBOARDMOUSE, CONTROLLER_GAMEPAD } Controllers_t; void Init(SDL_Window* pWindow); void InitSDL(const void *data); void SetActiveDevice(const Controllers_t& pDevice); Controllers_t GetActiveDevice(); const char* GetActiveDeviceAsString(); const char* GetGamePadDeviceAsString(const int pPort); const Sint8 GetGamePadCount(); void TestEvents(const SDL_Event& pEvent); void PollEvents(); // <- Use this function if you're not using SDL event polling. void Flush(const Controllers_t& pController); void FlushAll(); //==================================================================== typedef struct { Sint32 key; Uint8 button; bool bDown; bool bHit; } action_t; const float GetActionInput(action_t& pButton); void RegisterAction(const std::string& pActionName, Sint32 iKey, Uint8 iButton, bool bConistant); void ModifyAction(const std::string& pActionName, Sint32 iKey, Uint8 iButton); action_t& GetAction(const std::string& pActionName); bool LoadActionsFromFile(const char* pszPath); void UpdateGamePadStickAsMouse(const Sint32& pWhich, const Sint8& pAxis); } Actions can be bind to a with key and a button. bDown is used to check if the action is being held down while bHit is a flag to check if this is something that should be called once per pressed (Like a key to pick up a box or something.) One top of all this, Actions can have their key/button bindings changed via an XML file. Here's an example how to use this with Leadwerks.
    #include "Leadwerks.h" using namespace Leadwerks; #include "rinput/rinput.h" int main() { Leadwerks::Window* window = Window::Create(); RInput::InitSDL(window->hwnd); Leadwerks::Context* context = Context::Create(window); World* world = World::Create(); Camera* camera = Camera::Create(); camera->SetRotation(35, 0, 0); camera->Move(0, 0, -6); Light* light = DirectionalLight::Create(); light->SetRotation(35, 35, 0); light->SetShadowMode(0); Model* model = Model::Box(); model->SetColor(1.0, 0.0, 0.0); model->SetPosition(0, 0, 0); model->SetShadowMode(0); RInput::RegisterAction("moveforward", KEYBOARD_W, GAMEPAD_BUTTON_LSTICK_UP, true); RInput::RegisterAction("movebackward", KEYBOARD_S, GAMEPAD_BUTTON_LSTICK_DOWN, true); RInput::RegisterAction("moveleft", KEYBOARD_A, GAMEPAD_BUTTON_LSTICK_LEFT, true); RInput::RegisterAction("moveright", KEYBOARD_D, GAMEPAD_BUTTON_LSTICK_RIGHT, true); RInput::RegisterAction("jump", KEYBOARD_SPACE, GAMEPAD_BUTTON_A, false); RInput::LoadActionsFromFile("controller.xml"); while (window->Closed() == false) { RInput::PollEvents(); float x = 0; float z = 0; float u = RInput::GetActionInput(RInput::GetAction("moveforward")); float d = RInput::GetActionInput(RInput::GetAction("movebackward")); float l = RInput::GetActionInput(RInput::GetAction("moveleft")); float r = RInput::GetActionInput(RInput::GetAction("moveright")); if (u != 0) { z += 0.05 + (u /10); } if (d != 0) { z -= 0.05 + (d / 10); } if (l != 0) { x -= 0.05 + (l / 10); } if (r != 0) { x += 0.05 + (r / 10); } model->Move(x, 0, z); x = 0; z = 0; Leadwerks::Time::Update(); world->Update(); world->Render(); context->DrawStats(0, 0, false); context->Sync(false); } return 0; }  
    The Result:
     
    There are many things I didn't add for the sake of time or I wasn't sure how to implement like supporting action for multiple controllers on the front end. But this does what I was set out to do, and although I haven't tried, I'm confident that with little to no work, this code will work on macOS and Linux. As long as there is SDL2 on the platform, this will work.
    Now no matter if the project is Leadwerks, Turbo or just an SDL2 project, I now have a portable input system with controller support which you can find here.
  23. reepblue
    Luawerks has been updated to 1.2.7, making some small adjustments and fixes to the system. If you have previously purchased Luawerks, this update is available for free on the Leadwerks Marketplace. Please note: Luawerks is not ready for Leadwerks 4.6 and it's recommended to use Luawerks with Leadwerks 4.5.
    Following changes include:
    Added an optional Map selection menu. "New Game" will default call a panel in which all the maps in your Maps directory will be listed, Note: This will only work if the maps are not included in your zip as raw Lua functions are called. This will also not work if the Lua sandbox is enabled. Re-located error.mdl to a sub directory "Models/Developer" Added widget.mdl and axis.mdl for assistance for visualizing matrix results. When in-game, "Exit to Menu" will show instead of "Quit." This will release the world and the player will be returned to the main menu. Arrange locations of various functions. The names remain the same. Added various functions needed for this update.  

    About Luawerks
    Luawerks is a Lua framework for video games developed on the Leadwerks Game Engine. It supplies developers with additional functions, handles the game loop and allows them to debug their code with a developers console. With all that out of the way, you can focus on making your game fun!
    You can purchase Luawerks from the Leadwerks Marketplace for $9.99.
    For documentation and bug reporting, please visit the GitHub page.
  24. reepblue
    I've uploaded a new release last night correcting the player's pickup to be much more reliable. It should be very rare for a case in which the player is in a object and releases it causing the player to fly backward. I could have kept objects solid but that caused another can of worms. I also posted this on my OneDrive due to issues of people not being able to download it from this site.
    Yesterday I was working on implementation of the power balls and the launcher. I've written a rough draft of this a few weeks ago and I had it fully working in normal player code. When I started to work on the VR version, that's when I started to run into some problems. First, I didn't like how the code was looking. It involved a lot of referencing of entities and classes. What made it hard is that the VR implementation was different from the normal players. What didn't help was after too many restarts of my application with VR enabled, the entirety of Steam would just crash. So developing for VR was very frustrating.
    With Turbo coming a long, I wanted my code to work with little modification as possible. I've written a function wrapper that'll only be called if the application is ran under Leadwerks. This both will save time in replacing these functions when the time comes, and get's me used to the new functions. One thing I'd still have to fix is how Lua is implemented in the new engine. Most of the heavy functionalities are in classes which will defiantly keep self.entity for referring to the entity it's being used on. However somethings will still need to be changed and there are a lot of unknowns right now.
    With that being said, I think it's safe to say it'll be best to focus on the art and assets going forward. With the amount of time I have to make these assets, I'm sure more clarification with Lua scripts with the new engine will surface, maybe even a beta to play with. But with it being Leadwerks or Turbo, they both need assets. VR mode will also be dropped for now. It'll still be there, just not fully supported. You'll need to run the app with -devmode to see the VR tab now. After the project transitions into Turbo, VR development will continue.
    I shall be going back to my Linux machine with my ultra wide color calibrated monitor to work on these assets. I've created a lot of Vectronic themed stuff in the past and I'm going to go through it and see what I can savage from it. All my models are done in Blender, and I want to create all textures in GIMP this way my files can be used with free and open sourced tools. Also, if there is any Linux developers who wish to use this package, they don't have to be hit in the face with Maya and Photoshop files.
    Releases will slow down, but I'll be posting screenshots of my work soon.
     
  25. reepblue
    For who those who don't know (or remember) Vectronic was my first person puzzler project I was developing from 2013-2016 starting with the Source Engine and then onto Leadwerks. The goal of the project was to create a puzzle game and allow people the assets to make their own puzzles. Although it never saw it's end, you can still play the demo here.
    So what happened? Vectronic was how I pretty much learned a lot about programing, art and game design in general. I kept going back working on it for a month or so and then stop because I would hit a road block with assets, code, performance, or time. I made a lot of cool things too that just never saw the light. Over the years however, I also noticed I don't have patience to work on one game alone for months at a time. I do however like to play around with stuff so I began to think: "What if I had a project for me to tinker with and post it publicly every so often?" I'm more busy than I was in the past so time is more limited. However, I've probably written Vectronic game code over 50 times and I now know the right way of doing such. So what's the plan?
    After some experimentation, I've decided to go ahead and start a new project taking everything I did and learned with Vectronic and making a base template for people can use for their own purposes. My packaged game will be free to download and play (Because it's just me playing around) while the assets and will be posted on the Leadwerks Marketplace for a charge.The package will contain all assets to build your own test chambers, and you'll also get the original source files too! My release game will act as an outlet to my tinkerings and provide as an advertisement to the asset pack.
    With the code, the idea is that everything will be modular and users can easily create their own power balls to do various of things. The power balls can now be applied to any entity under certain conditions so there is no need for a VecBox.lua script anymore. There will be plenty of examples for many to see how things were done. I learn best by references and examples, and having code, art, and maps accessible should help new users into learning game design, or aiding developers in assets to start their ideas.
    I've decided to change the name to Vec-Tec as the asset pack will be in the Vec-Tec Research Center. Also with the new approach, I think it's time to drop the name Ive been using for four years. You can give Vec-Tec a try right now here!  It's very minimal, there is no art, but the foundation is there. VR is now gonna be a thing with this, If you have a VR HMD, you can play any map in Roomscale VR!
    I hope this is a start of a remarkable development. I hope you are interested and see value in this  project. You can find the project page here for the latest updates and releases.
×
×
  • Create New...