Showing posts with label Cpp. Show all posts
Showing posts with label Cpp. Show all posts

Feb 5, 2025

Sync Engine

Team of 4
September 2024 - Current
Skills Utilized: C++, Multithreading, Reflection, Serialization, Profiling & Optimization, Game Engine Programming

Introduction

For my senior year project, I set out to build a fully-fledged game engine. While I had prior experience with game engine development, I had never created a complete engine from the ground up and wanted to take on the challenge.

The engine was developed in C++20, utilizing CRTP to elegantly collect type information. This allowed the engine to automatically gather information on all subclasses of Actors and Components. I designed a highly intuitive serialization and reflection system, requiring only a single-line declaration per class while ensuring compatibility with field reordering, addition, removal, and even virtual inheritance. To support fast iteration, I implemented DLL hot reloading for game code. Additionally, I developed an intrusive profiler module using assembly to automatically track execution times for all functions, which helped optimize serialization performance by up to 1300%.

I handled all aspects of the engine except for graphics backend, which were delegated to two graphics programmers.

Showcase

↑ Overall Demonstration

↑ DLL Hot Reloading Demonstration

Technical Highlights

Separate Graphics Thread

To improve both game logic and rendering performance, I separated the render thread from the game thread and built a dedicated communication layer between them. When registering graphics objects, I implemented a double-buffered approach: two lists were maintained, with the render thread processing one while the game thread added new objects to the other. Once the render thread completed its work, the lists would swap, ensuring that neither thread had to wait on a lock.

Additionally, the editor, built using ImGui, required updates to be processed on the render thread due to graphics constraints. However, modifying actor or component fields directly from ImGui led to data races. To resolve this, I redesigned all value modifications as commands that were stored in a buffer, allowing synchronized access between the game and render threads. This solution later proved invaluable also when implementing real-time collaborative editing.

Reflection

I designed an intuitive reflection system for actors and components using template metaprogramming and macros, making field reflection and serialization effortless. For example, the reflection code for a Transform Component looks like this:


Fields that need to be reflected or serialized are listed once inside the class and once externally. Additionally, editor-specific configurations like specifying Getters/Setters can be done in a single line, keeping the process simple and intuitive.

Serialization

Level data is stored in a binary format while ensuring that field reordering, addition, or removal does not break compatibility with previously saved data. This is achieved by maintaining type metadata for each class. The system stores a hashed name and type ID for each field, allowing it to compare stored field signatures with the current structure at load time to ensure proper deserialization.

↑ Type information structures

Additionally, pointer fields for Actors/Components and Assets are handled seamlessly. Actor and Component pointers store memory offsets, ensuring that objects are reconstructed at the correct locations during loading. Asset pointers, on the other hand, store asset references (paths) instead of raw pointers, preserving data integrity across sessions.

DLL Hot Reload

To enable hot reloading for game code, the engine compiles the game logic into a DLL, which the engine executable loads dynamically. The engine build process produces both an EXE and a LIB, and the game DLL links against this LIB.

A major challenge in this setup was handling global variable ownership between the EXE and DLL. For instance, if the Game class has a global variable and it is defined in a header, both the EXE and DLL end up with their own separate instances. Even using extern and defining it inside the engine’s source files doesn’t solve the problem—since the engine’s LIB is linked to the game DLL, the DLL will still create its own instance.

The solution I devised was to use preprocessor directives to control the definition depending on whether the engine or game is being built:
During the engine build, the header file defines an inline instance. During the game build, do not declare the instance. Then, both for the engine code and the game code, use a pointer to that variable exclusively. This pointer is declared as such using the preprocessing directives:
Engine: extern "C" __declspec(dllexport) inline Game* GameInstance = nullptr;
Game: extern "C" Game* GameInstance;


Using a pointer instead of a direct instance prevents link-time errors in the game build due to missing definitions.

At runtime, the appropriate instance is assigned based on whether the ownership should reside in the engine or the game, and I implemented a helper function to automate this assignment, ensuring clarity and preventing misuse.

↑ List up the global variable to be shared,

↑ And this function will automatically synchronize the pointers.

Intrusive Profiler & Optimization

I developed a profiler that automatically records the execution time of all functions without requiring any additional code from the user. This was achieved by writing _penter and _pexit in assembly and enabling the /GH and /Gh compiler options to ensure these functions are executed at every function entry and exit.

One key challenge was preventing the profiler from recursively profiling itself. To avoid this, the profiler was built as a separate DLL, ensuring its code wouldn’t be instrumented. This required adding EXPORT after the _penter PROC declaration.

↑ Part of the profiler assembly

↑ Example profiling output

Using this profiler, I discovered that directly writing to fstream during serialization was creating a major bottleneck. To address this, I leveraged a custom memory manager to accumulate all data first and write to fstream only once at the end. This optimization resulted in a 1300% performance improvement.

Conclusion

This project allowed me to tackle complex engine systems and refine my problem-solving approach. Designing a reflection-based serialization system that remained compatible across versions, implementing a multi-threaded rendering pipeline, and handling global state in DLL hot reloading were all challenges that required careful consideration.

Beyond technical implementation, the experience reinforced the importance of profiling and optimization, as seen in the significant performance gains achieved in serialization. It also highlighted the necessity of maintainable and scalable design choices.

The lessons learned from this project will inform my future work in engine development, particularly in balancing flexibility, performance, and ease of use.

Feb 4, 2025

Mercy for Machines

Team of 13
September 2023 - April 2024
Skills Utilized: C++, Unreal Engine, Tools Programming

Introduction

Our team, consisting of programmers, designers, artists, and sound designers, collaborated to develop a game using Unreal Engine 5.2. Mercy for Machines is a 3rd person, narrative driven sci-fi horror game. My primary role was gameplay programming, and I also developed editor tools for designers and implemented destruction mechanics using Chaos physics.

Showcase

↑ Actor Collector

↑ Chaos Destruction

Technical Highlights

Actor Collector

While observing the map creation process, I noticed that manually maintaining lists of various actors was a frequent and inefficient task. To streamline this, I developed a tool that automatically collects and visualizes actors. Users can intuitively select actors using box and sphere volumes, with additional filtering options based on actor class, interface, or component class. To enhance visibility, I implemented an outline material for collected actors and a material effect that desaturates everything else in the scene. To minimize runtime overhead, the tool only retains the collected actor list in the final build without performing any additional operations.

Chaos Destruction

One gameplay sequence required a staircase to collapse, causing the player to fall. I immediately thought of Unreal Engine 5’s Chaos physics engine and its destruction system. While fracturing the mesh was straightforward, controlling how the pieces scattered proved challenging. I achieved the desired effect by carefully utilizing Anchor Fields and Master Fields to direct the fragments’ movement.

Conclusion

Mercy for Machines was a valuable experience that expanded my understanding of Unreal Engine 5. Developing the Actor Collector tool improved level design efficiency, while implementing Chaos Destruction deepened my knowledge of the engine’s physics system. Through this project, I gained hands-on experience with Unreal’s editor scripting, rendering, and physics features, further refining my ability to develop both gameplay mechanics and engine tools.

Feb 3, 2025

3D Graphics Demo Program

Solo Project
March 2023 - May 2023
Skills Utilized: C++, OpenGL, JSON5, ImGUI, Graphics Programming, Game Engine Programming

Introduction

This was a course project exploring various graphics techniques. I implemented Toon shading, Shadow mapping, Noise-based terrain generation, Procedural mesh generation, Catmull-Rom splines, and Hermite curves.

Since the project required handling multiple scenes and shaders, I developed a simple data-driven engine that allowed assets like shaders, textures, and models to be registered and scenes to be configured dynamically at runtime through data modifications.

Showcase

↑ Toon Shading Showcase

↑ Shadow Mapping Showcase

Technical Highlights

Toon shading

This was the most enjoyable topic. The technique involves mapping the difference between the normal vector and light direction into discrete steps rather than a smooth gradient, creating a cel-shaded look seen in some games. Implementing it myself was fun, and the unique look and feel were refreshing.

One interesting challenge was mitigating aliasing artifacts at the boundaries of shading steps. A naïve implementation results in noticeable stair-step artifacts along the edges. To smooth these transitions, I used fwidth in the shader to blend the two colors over a few pixels near the boundary, reducing the harshness of the effect.

Shadow mapping

Generating shadow maps was also an interesting experience. While there are various advanced techniques, I implemented a classic, straightforward method for learning purposes. The scene is rendered from the light’s perspective to create a depth buffer, which is then used to determine shadowed areas based on depth comparisons and normal alignment.

Data-driven engine

Given the project's need for frequent scene and shader modifications, I developed a simple engine to manage objects and scenes efficiently. It automated scene and object handling and allowed assets such as shaders, textures, and models to be configured in a data-driven manner, enabling runtime modifications without recompilation.

↑ The tessellation scene's json5 file

Conclusion

Through this project, I explored various 3D graphics techniques and gained hands-on experience implementing them from scratch. I found toon shading particularly engaging, as it involved both technical problem-solving and aesthetic considerations. Shadow mapping reinforced my understanding of depth-based rendering techniques, while building a simple data-driven engine helped me appreciate the importance of flexible scene management. Overall, this project deepened my understanding of graphics programming and engine architecture.

Feb 2, 2025

Cartograph

Solo Project
December 2024 - Current
Skills Utilized: C++, Unreal Engine, Concurrent Programming, Multiplayer Programming

Introduction

I really enjoyed playing Satisfactory, but I always thought it would be nice to see all my buildings I built on the map. This led me to create a mod, Cartograph, using the excellent framework, Satisfactory Mod Loader (SML).

I leveraged the speed of rendering textures and shapes on a render target to draw buildings on top of the existing map. To improve user experience, I added features such as toggling building visibility and filtering by height, seamlessly integrating with the base game.

Since buildings can number in the thousands, I optimized the mod by spreading the rendering across multiple frames and only redrawing the affected sections when buildings are placed or dismantled. Additionally, I made it to fully support multiplayer.

At the time of writing, the mod has reached over 18,000 downloads and has the highest conversion rate among mods, with a view-to-download ratio of about 4:1.

Satisfactory Mod Repository (Where the mod is posted): https://ficsit.app/mod/Cartograph

Showcase

↑ The game's map with Cartograph. It draws the buildings, with a menu for toggling each building and filtering by height.

↑ The game's map without Cartograph. It doesn't draw any building.

Technical Highlights

Distributing Work Across Frames

Processing building data and rendering it on the map needed to be done asynchronously to avoid disrupting normal gameplay. For that, I wanted a method that allowed pausing midway while retaining local state, making coroutines an ideal choice. Using UE5Coro, I structured the rendering process to keep a per-frame time budget while maintaining a manageable code.

Partial Redraw Optimization

When buildings were placed or removed, redrawing only the affected areas was necessary for efficiency. To determine which buildings overlapped efficiently, I used a quadtree to store building data. However, simply redrawing those buildings led to misalignment at the boundaries. To solve this, I applied scissoring while drawing buildings on the render texture. Since Unreal Engine’s render target draw functions didn’t support scissoring, I created a custom class inheriting from FCanvasBaseRenderItem and manually called RHICmdList.SetScissorRect in the Render function.

Multiplayer Support

In multiplayer, clients only receive data for buildings near them for optimization, but the map needs to render all buildings. To support this, I had to implement a custom synchronization system. A major challenge was the large packet size required to send all building data at once. To resolve this, I split the data into smaller chunks and implemented a back-and-forth communication system between the client and server to receive the data over multiple packets.

Conclusion

Through this project, I gained experience in asynchronous rendering, spatial data structures, and network synchronization. Implementing coroutine-based rendering improved my understanding of managing time-constrained tasks efficiently. Working with quadtrees and custom rendering logic deepened my knowledge of optimizing large-scale dynamic rendering. Additionally, handling multiplayer data synchronization reinforced my skills in packet management and client-server communication. This project provided valuable insights into optimizing both performance and usability in a real-time environment.

Blinded

Team of 5
March 2023 - June 2023
Skills Utilized: C++, OpenGL, GLSL, JSON5, Graphics Programming

Introduction

Blinded is a boss rush-focused souls-like action game where the player restores red, green, and blue to a colorless world by defeating powerful bosses.

In this project, I developed a fragment shader for dynamic color removal and restoration, eliminating the need for multiple textures. I also implemented data-driven enemy and level design, streamlining iteration and balancing. These technical solutions enhanced both the game's artistic vision and development efficiency.

Showcase












Technical Highlights

Data Driven

I have transitioned various game elements, such as monster AI parameters, collision boxes, and level layouts, to a data-driven system using JSON5. This allowed for easy modification and extension without strict formatting constraints. By declaring a struct, exporting its members, and calling a parsing function, we could seamlessly populate in-game data with minimal effort.

Defining enemy attributes and placements in external files eliminated the need for recompiling or relaunching the game, greatly accelerating our iteration cycle. This significantly improved efficiency when fine-tuning enemy behaviors and level layouts.

↑Example of a level layout

↑Example of an enemy's AI parameters

RGB Fragment Shader for Dynamic Color Removal


To optimize asset management, I replaced pre-drawn grayscale and partial-color textures with a fragment shader that dynamically removes red, green, or blue from a full-color image. This eliminated the need for multiple texture versions, significantly reducing asset workload.

A key challenge was ensuring colors were removed correctly rather than simply setting RGB values to zero, which would turn everything black. Instead, we needed to gray out the removed components proportionally.

To achieve this, we used the HSV color model, which represents color using hue, saturation, and value. The hue determines the color type. By measuring the hue's proximity to the removed color, we calculated how much of it should be grayed out. The closer a color was to the removed hue, the more it was desaturated.


Rather than directly desaturating the color, we shifted the hue outside the removed color range. This ensured that a pure yellow (which contains both red and green) would still retain some green when red was removed, preventing incorrect full desaturation.
The final result was an interpolated blend between the original color and a grayscaled version based on how much of the removed component was present.

↑ Part of the final shader

This approach not only streamlined asset creation but also enabled greater gameplay flexibility, allowing players to dynamically regain colors in a non-linear progression.

Conclusion

Through Blinded, I gained valuable experience in shader programming and data-driven design. The shader development process deepened my understanding of color manipulation in graphics programming, particularly when working with RGB components and HSV models. Additionally, transitioning the game’s enemies and levels to a data-driven approach taught me how to optimize workflows and speed up iteration cycles, greatly improving both development efficiency and gameplay balance. This project enhanced my problem-solving skills, especially when faced with complex technical challenges and the need for creative solutions.

Aug 26, 2023

Typoon

Solo Project
April 2023 - December 2023
Skills Utilized: C++, Multithreading, Windows Programming, Tools Programming, Unit Test

Introduction

Typoon is a unique text expander designed specifically for Hangeul, the Korean script. Most of the text expander out there doesn't really work well with Korean typing system, so I ended up making one myself. It supports the alphabets as well. It was heavily inspired by espanso.
In this project, I designed and implemented efficient data structures and algorithms to store and process text data, accurately detect letter composition, and correctly replace input text. Leveraging Windows programming, I enabled automatic file change detection and user notifications for convenience. To ensure reliability, I implemented comprehensive unit testing, overcoming the testing framework’s lack of wide character support.


What is a text expander?

"A text expander is a program that detects when you type a specific keyword and replaces it with something else. This is useful in many ways:
- Save a lot of typing, expanding common sentences.
- Create system-wide code snippets."

 -- From the espanso's readme file

Showcase



↑ Typoon comes with a convenient system tray icon

↑ You can write your own matches intuitively

Technical Highlights

Managing the trigger strings

In a nutshell, Typoon builds a "trigger tree" with 2 iterations to manage all the match data.
Let's say there are 4 matches:

Within the first iteration, Typoon actually builds a tree with the given data. While building, there can be 'stale' nodes that cannot be reached. Those nodes will be discarded in the second iteration.

In the second iteration, the tree gets flattened in level order to maximize the locality of reference and minimize the memory usage. This is the final form of the data that is used in the runtime.

Detection of Korean letter composition & IME simulation

Understanding the computer-based composition of Korean letters is essential before diving into this topic. A Korean letter comprises three components: an initial, a medial, and an optional final. Unlike alphabets, where one keyboard press corresponds to a single letter, forming a Korean letter may require up to five keystrokes. Hence, capturing keyboard input alone is insufficient; knowing the actually composed letters is crucial.

Source: https://rcneira.weebly.com/learn/reading-the-korean-alphabet-hangul

You might ask, "Why not decompose all the letters and use that to match the input?" Unfortunately, this approach often leads to an ambiguity. For instance, decomposing "곡" into "ㄱㅗㄱ" might inadvertently match "고가," which decomposes as "ㄱㅗㄱㅏ." This mismatch is usually undesirable. Well, matching the composed letters is the only option it is, then.

Acquiring the composed letters presented two options. First, extracting them from the Input Method Editor (IME), a layer for Korean composition. Second, simulating the IME to compose letters within Typoon based on the keyboard inputs.

Obviously, I went for the former one at first. But after days of researches, I've come to a conclusion that there is no way to get the letters from either the processes or the IMEs attached to them. You simply cannot access to the other processes' IMEs, and there is no unified way to get the composed letters from all kinds of processes.

So, the only option was to simulate the IME myself. It was a pretty deep case analysis, but I managed to figure it out.


File watcher

Typoon incorporates a file watcher feature capable of promptly detecting and implementing changes to both the config and match files upon saving. This functionality leverages Windows-specific functions: WaitForMultipleObjectsEx() and ReadDirectoryChangesW().
In essence, you put a thread into sleep and tell it to wake up if any of the watching events triggers(WaitForMultipleObjectsEx), and you tell the OS to trigger an event when a change is made to a given directory(ReadDirectoryChangesW).



Addressing the lack of wide character support in doctest

Typoon's focus on Hangeul and Windows necessitates the use of wide characters throughout its codebase. However, most unit test libraries often fall short in handling wide characters, resulting in character display issues like '�'.
So I decided to settle on one and devise a workaround. By delving into doctest's result printing functions, I identified a solution. I introduced a template specialization of a class responsible for converting objects to strings. In addition to that, given that I employed diverse string types for wide characters, such as std::wstring, std::wstring_view, wchar_t[ ], and others, I ensured that the specialization accommodate all these variations by utilizing the std::convertible_to concept.

Conclusion

Typoon has been a particularly enjoyable project for me, offering numerous valuable learning experiences. I gained expertise in Windows programming, realized the effectiveness and the importance of the unit testing, learned how to build an installer for Windows, delved into original data structures and algorithms, got familiarized with multithreading, and the list goes on.
Even though I'm having a temporary break right now, I will definitely pick it up again soon in the future.

Nov 11, 2020

Q

Team of 5
September 2020 - December 2020
Skills Utilized: C++, OpenGL, Python, Graphics Programming, Scripting System, Template Metaprogramming

Introduction

During this semester's GAM class, my group decided to create a 2D action platformer game. I was responsible for constructing the graphics layer of the custom game engine using OpenGL and establishing a Python scripting system.
Built an object-oriented graphics layer encapsulating OpenGL concepts like meshes, textures, and shaders. To enable fast iteration, implemented a Python scripting system by embedding Python into the engine and exposing engine functions using template metaprogramming.

Showcase

↑ Prototype Demo

↑ Graphics showcase




↑ Scripting system showcase







↑ Scripting system showcase (integrated with the engine)




GitHubFunction Property Inspector (Part of the Python Embedder)

Responsibilites

  • Technical Director
  • Graphics Programming
  • Scripting System
  • Part of the Engine Programming
  • Other Gameplay Programming

Technical Highlights

Graphics layer of the engine

One of the challenges when working with OpenGL in an object-oriented language is that it functions as a giant state machine. While efforts have been made to encapsulate some aspects in version 4.5 and above, the core inconvenience remains. Consequently, during the design phase of the engine's graphics layer, I prioritized an object-oriented approach. I encapsulated each distinct concept, such as mesh, texture, and shader, in its own class to maximize encapsulation.


Python scripting system

Last semester, an assignment required the application of template programming. I decided to develop a Python embedder for personal exploration. And in this semester, for the game project, I decided to upgrade and use this embedder instead of relying on existing solutions, well, just for fun.
It automates the cumbersome process of registering C++ functions for using them in Python. All you have to do is one macro or a function call, depending on your preference. It heavily uses template metaprogramming for that automation.
One biggest challenge was iterating over the variadic template parameters. To expose a function to Python, you need to make another function that takes in the PyObjects, parses them then calls the original function with the parsed objects.
I was making that part of the automation and I needed to parse an arbitrary amount of arguments, so I was taking the list of the parameter types as a std::tuple. I needed to iterate over them to parse them. I couldn't use a for loop since the index of the std::tuple_element_t should be complie-time constant.
What should I do? The only thing I know in extra is the count of the parameters. After many trials and errors, I managed to figure out a solution. Make an index sequence with that count, take them with std::index_sequence<ArgumentIndices...>, and call a function for each of them with { parse<Tuple, Indices>(args)... }.




With this, I was able to register all different kinds of functions with various parameter lists with a single function call.

Conclusion

It was fun to design and build a graphics layer of the engine and implement a scripting system, which allowed us to iterate faster on the gameplay programming. Throughout the project, I gained valuable insights into OpenGL and template metaprogramming, as well as the integration of a scripting language into a game engine. These experiences collectively contributed to a significant enhancement of my skillset.