After months of programming, the time has come again to show off what our engineering team has been up to. A lot of under the hood architecture has been re-written, drastically reducing memory usage and improving frame rate. Meanwhile a brand new UI system and editor has emerged. This is more of a technical update than visual so pretty pictures will make a return next time.
A Huge Thank you to our Patrons! Your contributions make Bright Engine Possible!
Zeppot • Daniel Burkhart • Massum Zaidi • Mark Makarainen • Çağlayan Karagözler
New Feature: Universal Resource ManagerUntil now, Bright Engine has handled resource management pretty poorly. While there were systems in place to prevent double loading of texture and audio data, each system operated independently. Apart from making the codebase far more cluttered, the existing method was pretty useless when it came to handling model data.
All too often, duplicate data would be loaded into memory even though it wasn’t needed or ever used. And in some cases, it even caused tiny memory leaks that built up in the background. This was particularly noticeable when switching between different zones.
Needless to say, that’s not good. But rather than continue with the bandaid solutions we’ve been using so far, we decided to rip out the old system entirely and put in a new one. And the results are pretty encouraging with a 25% drop in memory usage.
So how does it work? Originally, when an asset was loaded, the class would compare against a vector that contained all previously loaded assets of the same type. If it didn’t find an entry that matched the asset that was being loaded, it would be added to memory, otherwise return a pointer to the address in memory where the data has already been loaded.
While it works, there are a few problems with this approach. Firstly by performing all the memory management inside the classes of the assets, it created a lot of duplicate code and resulted in larger class sizes. That’s bad news for maintaining the project as well as performance.
Secondly, this method created chaos with the 3D Model data that caused the instanced rendering system to have a meltdown, if model assets were cleared from memory before being cleared from materials. If we wanted to continue this method, it would have created a complete mess of spaghetti code that would have been a nightmare to maintain as well.
Thirdly, when it came to removing data from memory, because instances weren’t tracked, removing specific assets constantly led to nullptr crashes. To solve this, when switching zones, all data was cleared and then reloaded, including assets that are present in both zones, leading to longer loading times.
So, how did we fix this? The answer is pretty simple. We created a universal memory manager.
Asset loading is now completely handled by this memory manager which returns a pointer to the requested data. And unlike before it also keeps track of how many instances of this data is being used. If the number of instances falls to zero (meaning the data is not being used by anything anymore), and it's not being reserved by the user, it will automatically clean it out.
This is the start of our data-streaming system. A crucial first-step to extending Bright Engine’s open world support.
New Feature: Rewritten Object ArchitectureWe’ve already touched on the successful reduction of object class sizes by making the resource manager handle all the loading side of things. But even with this improvement we decided to take it a step further and completely rewrite how Bright Engine handles editor objects.
For clarity, we’re referring to objects such as 3D models, lights, waypoints, emitters, audio zones, etc. The old system kept each object type independent. By taking this approach it allowed for much faster searching regarding object selection.
However, to handle object selection and multi-selection efficiently, a Universal Object class was needed. All this system did was have a bunch of pointers set to null that would be updated to point to the object that has been selected.
But, this creates a problem. Whenever trying to access the object, a check needed to be performed against the object type so the correct pointer within the list of pointers could be referenced. In other words, whenever accessing the Selected Object a whole bunch of cpu cycles would be wasted just to determine its type. Even if we stored the type in advance as an enum or integer, a switch statement to return the right pointer would still need to be performed. In simple terms, this was really inefficient and hurt performance.
So how did we fix this? It’s simple, we completely deleted the Universal Object class, and created a new one. However, this time all of the editor object classes, inherited from this new Universal Object class. What’s more, for the properties and variables each editor object shares, like position, rotation, scale, waypoint assignment, locked status, Object ID, etc were moved into the Universal Object class.
This means that our Model class only stores properties and variables specific to 3D models. The same applies for Sound Zones, Lights, Cameras, Targets, the whole shebang. This decision alone cut class sizes in half. And best of all, by using inheritance it eliminated the need to perform any type checks when accessing a selected object.
Combining the elimination of continuous type checks with reduced class sizes resulted in a significant cut in work for the CPU, sending the frames per second up significantly.
New Feature: UI Library
One of the most requested features from the community has been to implement the user interface system. And going into this update, the original plan was to outsource this to a pre-existing UI library. However, after experimenting with some of the more popular open-source packages, like Dear ImGui, and Crazy Eddie’s GUI we quickly discovered, this wasn’t going to be a viable long-term option.
Consequently we were left with no choice but to develop our own UI system. Generally speaking building a UI system isn’t all that complicated. But we very quickly discovered that it's a bit of a rabbit hole. With so many ways to extend functionality and styling, the parameters for components are frankly endless.
As a result, this first version of the UI library unimaginatively named BrightUI is unfinished. But that doesn’t mean we haven’t added a lot of functionality already. So let’s explore how it works.
Each section of a game interface, for example, the main menu, or player inventory, is broken up into different files. We call these UI Elements, and it helps keep things organised. Each element can contain an infinite amount of widgets which are used to create the actual interface. In this update a total of 14 widgets to work with, but we intend to add a lot more moving forward.
|Button||Check Box||Combo Box||Label||List Box|
|Panel||Progress Bar||Scroll Bar (Horizontal)||Scroll Bar (Vertical)||Slider (Horizontal)|
|Slider (Vertical)||Spin Box||Text Box||Window|
Of these widgets both Panels and Windows can act as Containers. Meaning other widgets can be placed inside them, and inherit position data. This is more commonly referred to as a Parent-Child System.
Going through the settings and parameters for each widget in these patch notes would take quite a while. But for those who are curious, a complete breakdown can be found inside the documentation.
In terms of styling, the engine provides complete control over appearance. Widgets can be styled using flat colour values for their background, foregrounds, borders, etc. Or a texture can be used on each individual component. For users looking to maximise performance, Atlas Maps are also supported to reduce the number of texture binds. And animated texture support will be added in the future.
The UI system also has a state machine working in the background. As of this update, UI widgets can exist in Normal, Disabled, Pressed, Checked, NormalHover, or CheckedHover states. Different styles can be applied to a widget depending on the state it’s in.
To save time, BrightUI has a stylesheet system that enables users to specify how they want certain widgets to appear by default. Note that even with a style sheet applied, style settings for individual widgets can still be overridden providing even greater control.
Linked into the state machine, each widget has a selection of events that can be fired if the right conditions are met. For example, a button has a Click Event which will trigger whenever a user clicks on that button. A script can be assigned to any event where game logic can be executed, bringing the UI to life.
New Feature: UI Designer
Having a UI library is a step in the right direction. But developers are rarely willing to create their interfaces through nothing but code. That’s why we built a new tool called the Interface Designer.
Simply put, it's a visual editor where users can construct their game interfaces without needing to touch any form of code (excluding writing event scripts). While this editor has plenty of room for improvement, it does allow the rapid creation and editing of complete interface systems.
Components can be selected from the Widget List or UI Hierarchy, and edited using the hundreds of settings. Just like the World Editor, there is a search box feature so users can quickly find and update parameters without needing to scroll endlessly.
We also built a mini-tool that allows users to quickly select UV coordinates from any texture (including Atlas Maps) rather than manually writing out coordinates wasting precious time.
The style sheet system is also integrated into the editor. These can be saved and loaded at any time in a non-destructive way. And if users want to share custom built style sheets, these can be exported into a standalone file that another user can just drag and drop into their own project.
As an extension to BrightUI’s parenting system, the Interface Designer has an inbuilt layer system, similar to what is commonly found in image editing software like Photoshop. Different widgets can be brought forward or sent back in the render queue to make sure nothing get unintentionally hidden.
Lastly when it comes to writing Event Scripts, we’ve added a few shortcuts to save a bit of extra time. Each UI Element has its own script file that can access other UI files if needed. All a user has to do is write a function and then set the name of the function in the Script Event settings. Alternatively, users can generate these functions directly from the Interface Designer using a useful hotkey.
The designer has easily been the most time consuming aspect of this update. We’ve had to skip past several features that we wanted to implement in the name of saving time. But rest assured these will be added in the future.
Improvements: Text RenderingEven though we’ve only just begun introducing a UI system to the engine, text rendering has actually been around for a while. We’ve previously used it in the diagnostics mode of the World Editor. However, when adding this existing system to BrightUI, we quickly discovered it was not going to be suitable.
As fancy as it was to have support for TrueType Font files, the rendering process for characters was pretty horrendous. That’s because each character or glyph was loaded into its own internal texture. Then when it was time to draw some text on screen each character would require their own texture bind. Suddenly a short sentence with 100 characters would have hundreds of texture binds sending performance down the toilet.
While using a texture array could help solve this problem, we opted for a more traditional approach of using bitmaps. This is by far a more performance option but it has one major drawback, because the bitmap is saved in pixel space, large font sizes tend to look horrendous as the pixel edges can easily be seen.
Fortunately, there is a fancy solution to this problem. It’s called Signed Distance Fields. These transform regular font Bitmaps into something that resembles a Heightmap. But using some fancy mathematics, we’re able to render large font sizes without any pixel artefacts appearing.
What’s more, Signed Distance Fields can also be used to generate fancy text effects such as Glow, Outlines, and even Drop Shadows at virtually zero performance costs.
Improvements: Code QualityWhile the UI system may have been the primary focus of this update, a lot of improvements have been deployed across Bright Engine’s code architecture. Stuff like the new Memory Manager, and Universal Object System have helped eliminate thousands of lines of code from the project without sacrificing any functionality.
If we want to be specific, 7824 lines of redundant or duplicate code have been wiped out. This makes the code base far easier to maintain in the future as well as speeding up development and build times. And there are still plenty of more improvements to perform moving forward.
Bug FixesWith Bright UI and the Interface Designer taking up the bulk of the development time for this update, not many existing bugs were squashed this time around. But we still managed to wipe out a few more bugs from the World Editor nonetheless.
- Fixed bug where Terrain Node Tree didn't clear old data when being re-generated (memory leak)
- Fixed bug where changing a setting in the Render Panel sometimes caused a crash
- Fixed bug where selected two audio objects sometimes caused a crash
- Fixed bug where adding Cameras into a group sometimes caused a crash
- Fixed bug where adding Targets into a group sometimes caused a crash
What's Next?We’ve added a lot of new systems, and rewrote old ones in this update. This has undoubtedly spawned a whole new wave of bugs and stability problems that weren’t uncovered during internal testing. That’s why the next update is going to continue focusing on UI as well as the scripting system, bringing both up to speed with the rest of the editor.
Meanwhile, we’ve also got plans to begin integrating a basic physics system into the engine - the final piece needed for a minimum viable product! Collisions, and a universal wind simulation system are the first on the list for implementation. And if everything goes according to plan, Bright Engine will be faster, smarter, and more powerful in the next few months.