top of page

Skyward Tactics

Project Info

Gameplay/AI Programmer
Strategy
Unreal Engine 
w/ AngelScript
50% part-time
over 10 Weeks
5 Programmers

Game Summary
 

Skyward Tactics is a prototype online co-op tactical rogue-lite game developed as a graduation project at FutureGames Stockholm. Our team of five programmers, including myself, utilized Hazelight's modified version of Unreal Engine 5 with AngelScript support.

In the game, character units are functionally identical for both players and enemy AI, differentiated only by their controlling entity—either an AI Controller or a Player Controller. Actions are implemented using a command pattern, enabling the ability to undo every action. The server-authoritative action system affects the game state, while a client-side visualization system, using a strategy pattern, creates the visual effects of actions taken.

AI Controller

Overview

At the start of this project, I took on the responsibility of developing the game's AI system. This was challenging because the AI needed to be scalable and adaptable to a wide array of abilities, including offensive, defensive, and utility actions. Additionally, our team aimed to make the project highly accessible for designers, ensuring that as much functionality as possible could be manipulated without delving into C++ code.

To meet these challenges, I chose to implement a system based on Utility AI. This approach allows all possible actions that the AI can take to be evaluated based on the current state of the game. I designed the system to use Considerations in the form of drag-and-drop data assets and ensured that creating new Considerations was possible through AngelScript.

By doing so, I made the system easily accessible to designers and extendable by more technically inclined designers via scripting.

What is Utility AI

Utility AI is a flexible and scalable approach to AI decision-making. Unlike hard-coded rules or rigid behavior trees, Utility AI uses a normalized scoring system for each possible move's potential gain based on various factors, called Considerations. These Considerations evaluate multiple aspects of the current situation, allowing the AI to compare different actions and sequences to find the most suitable one at any given moment.

Implementations Overview

To maximize performance, I implemented the overarching AI Controller in C++. For the Considerations, I used AngelScript, which allows for rapid iteration due to its almost instant compilation, even during runtime, and provides flexibility for expansion without the complexity of C++.

The key components of the AI Controller include:

​

  • Recursive Decision-Making Loop: Evaluates all possible actions and scores them using the Utility AI system.
     

  • Consideration System: Composed of small data assets, each evaluating a single factor and scoring it according to its internal logic. The system is accessible and expandable using AngelScript.
     

  • Action Execution: Executes the chosen actions using the game's built-in action system.
     

  • Highlight Actions: Utilizes special actions created specifically for the AI to visualize its "thought process" to the player before execution.

Custom Logging Solution

Overview

In addition to developing the AI system, I designed and implemented a custom logging system. The primary goals were to simplify debugging by focusing on our specific logs, separated from Unreal Engine's verbose output, and to serve as the backbone for the game's combat log feature. While not essential, this logging system made debugging clearer and more efficient. It also provided an easy way to save logs to a text file even in shipping builds, allowing us to review game sessions and verify functionality on different devices during network testing.

The core logger is versatile and can be integrated into any C++ project. I created a wrapper around it for usability within Unreal Engine, providing access from C++, AngelScript, and Blueprint scripts.

Design and Implementation

The logging system consists of two main components:

​

LogManager:

The core LogManager is independent of Unreal Engine, making it suitable for any C++ project. It maintains a cache of recent log entries, allowing quick retrieval and inspection without parsing the entire log file. This feature was initially added to support the in-game combat log system but ended up being primarily used for debugging.

It's designed to be thread-safe and asynchronous, ensuring that log messages are written without blocking the main game thread. This is achieved through the use of mutexes and a dedicated logging thread that processes the I/O operations for queued log messages separately from the main thread.

To prevent log files from becoming too large, the logging system includes functionality to rotate log files when they reach a defined maximum size.​​

UCLogManager:

The UCLogManager class is a wrapper class that makes the logging functionality accessible from Unreal Engine's UObjects, Blueprints, and AngelScript scripts. To simplify logging throughout the codebase, I defined a few simple macros for different log categories (e.g., Info, Warning, Error). Exposed functions enable access to the log system from both Blueprints and AngelScript, allowing for consistent logging across different parts of the project.

Reflection on Implementation

Implementing a custom logging solution provided significant benefits during development, even beyond its intended use for game features. However, integrating this system outside of Unreal Engine's bounds introduced some issues that I would need address before using this solution again.

Initially, the logging system was designed to operate independently of Unreal Engine's lifecycle, aiming to always be accessible and logging, even for events outside of play mode. This approach led to undefined behavior where the logging would stop working after exiting play mode on some occations. I suspect that the static variables, which can persist across play sessions in the editor, were the primary cause of these issues.

​

To prevent such problems in the future:

​

  • Lifecycle Management and Static State Persistence: While keeping the logging system independent of Unreal Engine's lifecycle management remains a viable option, it requires careful consideration and extensive testing. Static variables should either be closely aligned with Unreal Engine's lifecycle or reset as part of the logging system's lifecycle management. Managing static lifetime variables is essential to avoid conflicts and ensure proper functionality after exiting and restarting play mode.
     

  • Error Handling: Ensuring the logging system is robust and reliable requires comprehensive error handling. This includes managing file I/O errors, handling exceptions from thread synchronization issues, and safeguarding against crashes.

© 2024 by Johan Brandt

bottom of page