5 Classes That I Absolutely Can't Live Without!
I've been tinkering with Leadwerks 5 ever since it first went into Alpha. I enjoy writing abstraction classes and figuring out what's the best way to package a lot of functionality into a simple class. I most of the time have the right intentions but often fall flat on my face with the first attempts. Over time, I find myself rewriting and restructuring the classes over and over until I feel I have something solid like if it was part of the official API. In this article, I wish to share my top five classes that I can't live without!
Convars/ConCommands
Coming from Source, the lack of console commands was very alien to me at first. I was so used to being able to change data through a simple input box. This desire to have one goes all the way back when I started with Leadwerks 3. Eventually, I've written a pretty basic ConCommand system thanks to @Crazycarpet and I used it in Cyclone. There were no console variables (Cvars) as I thought they were pointless. Over time, I started to learn the difference between commands and variables and wanted to support both in Leadwerks 5.
They are both static members and functions in the application that get generated at run time. Here's an example of a console command.
void CC_Quit(std::vector<String> arg) { EmitEvent(EVENT_QUIT, NULL); } static ConCommand quit("quit", CC_Quit, CVAR_DEFAULT, "Quits the application");
Commands need a name, a description and can accept arguments, The CVAR_DEFAULT flag just tells the program what kind of command it is. There's also CVAR_CHEAT if you want the command or Cvar to only trigger if cheats are enabled.
void CC_MSAA(String arg) { if (arg.empty()) return; auto cam = GetCamera(); if (cam) { cam->SetMsaa(arg.ToInt()); } } ConVar cam_msaa("msaa", "1", CVAR_SAVE, "Sets antialasing mode.", CC_MSAA);
Here's an example of a Console Variable (CVar). It looks like a command but there's a slight difference here. First, a default value must be set. You'll notice that Cvars support CVAR_SAVE which the current value will be dumped to a file if SaveConVars(path) is used. Not all Cvars need a function callback. In this example, if msaa was called, the callback would apply the new msaa setting to the camera. The valve can act like a static member if that's all you need.
You exclude both commands and variables with ExecuteCommand.
const bool ExecuteCommand(const String& input, const bool quiet, const bool firecallback);
You can simply use this system for your settings file instead of using a json file.
Input System
When I was developing Cyclone, I knew I couldn't ship without a way for people to change their keybindings. I actually spent a large amount of time making a complex system to support not only bindings, but also multiple forms of input. The idea of the Input System is that in your code, you just need to call the action (such as "Jump") and the backend will figure out what key or button it is. When Cyclone actually shipped, I learned that Steam Input just takes over XInput functionality. The only two devices you'll need to worry about is the Keyboard/Mouse and Steam Input.
Steam Input is a whole can of worms, and the implementation needs to be suited to your game explicitly. Most projects just need a better alternative to using the hard coded window functions.
To create the input system, all it needs is the window to listen to.
auto window = CreateWindow(); auto input = CreateInputSystem(window);
Then you can load your bindings like so.
input->LoadBindings("keybindings.json");
These are the main commands for the input system, it's mostly a keyvalue system. There was an abstract system for action sets, but I didn't find them all useful.
void SetActionBind(const String& name, const int button); int GetActionBind(const String& name); const bool ActionDown(const String& name); const bool ActionHit(const String& name); const bool ActionReleased(const String& name); Vec2 ActionAxis(const String& up, const String& down, const String& left, const String& right);
The class is designed to be used as a singleton. This allows you to call the member from anywhere!
auto input = GetInput(); if input->ActionHit("Jump") { Jump(); }
ActionAxis turns for actions into a Vec2. This will make it easier to support controller sticks.
Vec2 move = input->ActionAxis("MoveForward", "MoveBackward", "MoveLeft", "MoveRight");
Mouse look for FPS games is the only thing that can be tricky. All I felt like I can do is provide alternative methods to avoid using ActiveWindow().
if (input->GetWindowActive() and freelook) { iVec2 center = input->GetWindowCenter(); auto mpos = input->GetMousePosition(); input->SetMousePosition(center); auto centerpos = input->GetMousePosition(); // Blah blah blah.... }
The input system is actually tied to the concommand system we've discussed earlier! You (or your players) can bind keys to commands or bind actions to keys with bind and bindaction respectfully.
Object Pools and Queues
These quick wrapper classes save a lot of copying and pasting. For Cyclone, I had a lot of cases in which I needed to manage entities in a vector. I didn't have to manage them much once added, but I needed to know what was added, what left, and not have things be pushed more than once.
ObjectPool does just this.
auto pool = CreateObjectPool() pool->Add(entity1); pool->Add(entity2); pool->Add(entity3);
Instead of writing that long std::find method to check if an entity is in the container, I use ObjectPool::Find(). It's a lot nicer and easier to use!
ObjectQueue is a little different. It's like ObjectPool but it only holds a certain number of objects. When a new object gets added to a full list, the old one gets removed. This is all thanks to std::queue. I used this in Cyclone for managing energy ball impact decals.
I currently have it set to delete the entity which can cause potential problems, I may just remove the object from the queue and allow for a callback.
Graphics Window
If you played Cyclone, you'll find a disclaimer on the settings window stating that you need to reset the game to see the window mode changes. This is because of the GUI class in Leadwerks 4 needing a window at all times. If you delete the window and/or GUI, the program will crash! The 2D drawing system works differently in Leadwerks 5. The system looks for a camera to render on to meaning you can destroy the window and framebuffer, and make a new one to render onto! We do this because it's currently not possible to resize the window with SetShape or any other functions with a framebuffer attached.
The wrapper class just manages the window and framebuffer pointers. Every time it's requested to "resize", the class will just delete the pointers and rebuild them. It also sends an event broadcasting the new sizes so any UI elements can adjust. The GraphicsWindow class also has the input system built in so there's no need to worry about that.
A neat feature is that I've added functionality to toggle between full screen mode with F11. This makes testing much more enjoyable and easier!
Player Camera
Last but not least is the PlayerCamera class. You'll run into a lot of situations where you need to locate the player's camera. That's easy, you just find your player component and retrieve the camera from that, right? Ok, but what happens if the scene clears and so does your camera. All the changes you've done to it need to be re-applied. This is a bigger issue when it comes to UI setup. It's probably best you have the player class as a global member.
Ok, but why does is abstraction class warranted? Well, there's a nice effect I like to have and that's the Outline effect. It takes a bit of work to setup and actually requires 2 additional cameras to the mix. It would be nice if I can just tell the camera to make something glow. And managing settings can be complicated being that some settings are stored with the world while the rest are stored with the camera. What about Post Processing and handling that? Not all players will be able to run the game with all the effects enabled! Plus, with post effects, there's a certain order they need to be stacked.
The PlayerCamera Class just compacts all of that into one wrapper.
// Create the camera. auto camera = CreatePlayerCamera(world) camera->SetPosition(0,0,-4) camera->TogglePostEffectState(POSTEFFECT_BLOOM, true); // Add a box, make it glow! auto mdl = CreateBox() camera->AddEntityToGlowList(mdl);
Settings are ether on/off or a settings level.
enum SettingMode { SETTING_DISABLED = 0, SETTING_LOW, SETTING_MEDIUM, SETTING_HIGH, SETTING_ULTRA }; // Settings virtual void SetFov(const float fov); virtual const float GetFov(); virtual void SetGamma(const float gamma); virtual const float GetGamma(); virtual void SetSsr(const bool mode); virtual const bool GetSsr(); virtual void SetRefraction(const bool mode); virtual const bool GetRefraction(); virtual void SetTessellation(const SettingMode setting); virtual const SettingMode GetTessellation(); virtual void SetMsaa(const SettingMode setting); virtual const SettingMode GetMsaa(); virtual void SetLightQuality(const SettingMode setting); virtual const SettingMode GetLightQuality(); virtual void SetShadowQuality(const SettingMode setting); virtual const SettingMode GetShadowQuality();
Like the input system, this is intended to be used as a singleton. If CreatePlayerCamera() is called another time, it'll just return the existing static pointer.
auto camera = ActiveCamera(); camera->SetMsaa(SETTING_HIGH);
Conclusion
After years of playing around with the engine. These are the types of classes I keep revisiting. Now, I feel like I got them perfect. They are self-contained and don't necessary rely on each other with the exception of the GraphicsWindow class and the Input System for convenience. I'm interested in hearing your feedback and suggestions for other classes you feel are necessary in projects!
Oh, did I mention these are all exposed to Lua?
-
1
0 Comments
Recommended Comments
There are no comments to display.