top of page

Ballad of the Raven and the Wolf

Project Info

Lead Programmer
AI and Systems Programmer
9 (2 Programmers)
Unity
Action, Survival, Co-op
4 Weeks

Project Summary

A foul presence has been twisting and devouring the wildlife of the Shivering Peaks. The Blight is spreading with an insatiable hunger. Only a hopeless Raven and a withering Wolf stand in its way - You

Throughout this project, I delved into extensive experimentation with Unity's scriptable objects. While also utilizing them conventionally to segregate data from logic, their usage extended far beyond. Consequently, their implementation might appear unconventional in certain areas.

I employed them to create a pluggable finite-state machine, housing decisions and states within ScriptableObjects. Similarly, I adopted a similar approach in devising an ability system for players, housing ability effects entirely within ScriptableObjects. This approach facilitated streamlined testing and rapid iteration, enhancing efficiency in development.

Contribution Summary

  • Enemy AI using a pluggable finite state machine.

  • Ability system for the playable characters, customizable by plugging in abilities in the form of Scriptable Objects.

  • Highly customizable spawn manager for creating and balancing enemy wave spawning.

  • Reusable health system both in the for of a Monobehaviour and Scriptable object.

  • Status effect system utilizing an IEffectable interface and a StatusEffect base.

Pluggable Finite-State Machine

My initial focus in the project was establishing a functional AI system for the game's enemies. Considering the, so far, undefined design for enemy behaviour, my priority was creating a flexible and adaptable system. I devised a pluggable finite-state machine, utilizing a Monobehaviour controller with interchangeable states. These states, encapsulating actions, decisions, and transitional states, were implemented as ScriptableObjects, allowing for easy modification and iteration.

The controller actively runs the States within the Update method, managing state transitions but deferring the actual call to the states themselves.

The State ScriptableObjects are all identical in code and differ only in their configurations through the plugged in Actions and Transitions. They each contain one array of Action ScriptableObjects, executed in sequence, and Decision ScriptableObjects that direct transitions to one of two States, potentially leading back to itself and without transitioning to a new state.

The states feature two core functions, triggered from UpdateState: ExecuteActions and CheckForTransitions. ExecuteActions iterates through the array of actions, executing them sequentially, while CheckForTransitions evaluates decisions for potential state transitions.

Example of a State: Attack Target State.
Example of a State: Attack Target State.

Each Action and Decision inherits from its respective base class. 

The base Action contains a singular function called Act, which all derived classes override. Each overridden function encapsulates the unique logic for that specific Action. Also since these actions are ScriptableObjects, they have the ability to possess their own set of exposed variables, allowing for customization within each individual instance.

 

Similarly to the action classes, the Decision classes feature a Decide function to override. This function evaluates the class-specific logic and returns a boolean value. This boolean serves as the basis within the state for determining the subsequent state to transition to.

Pluggable Ability System

Much like the AI System, I aimed for extensive customization within the Ability System via the editor. Borrowing structural concepts from the AI system, each player character contains an Ability Manager. This manager maps controller inputs to a ButtonAbility, serving as a serialized container that holds references to abilities and corresponding PlayerStates wherein these abilities are usable.

The core role of the Manager involves listening for button inputs, forwarding them to the relevant abilities, and overseeing the Initialization and Updating of these abilities.

Player Ability Manager on the Raven.
Player Ability Manager on the Raven.

The Abilities inherit from a base class featuring four abstract functions integral to the system's functionality.

The IsOnCooldown and InitAbility functions perform straightforward tasks, primarily checking cooldown status and initializing abilities, with minor adjustments specific to each ability.

However, MakeUpdate and Act are where the distinct behaviours of each ability emerge.

MakeUpdate, called from the PlayerAbilityManager's Update, exhibits varying behaviours across abilities. It manages cooldowns, buffering, over-time effects and more.

Act, responsible for executing the triggered portion of the Ability, on the other hand differs significantly between abilities, showcasing diverse behaviours, as illustrated in the examples below.

Raven Shoot Ability
Raven Shoot Ability
Raven Shoot Ability.

Around halfway through the project, the team aimed to create a three-part combo attack for the wolf character. To achieve this, I expanded the ability system, introducing a new ability designed to execute a sequence of abilities in order. This new ability, inheriting from PlayerAbilityBase, seamlessly integrates into the existing ability system and allows users to plug in any desired number of abilities to execute them sequentially, offering compatibility with all previous abilities within the system.

Editor for Multi Part Ability in use for Wolf combo.
Editor for Multi Part Ability in use for Wolf combo.
Three Part Combo attack for the Wolf.

Spawn Manager

The Enemy Spawn Manager is essentially a basic Monobehaviour class designed to continuously generate waves of enemies from various spawn points across the map. Its standout feature lies in its accessibility for designers, allowing easy manipulation and iteration of the game's difficulty.

Regarding difficulty adjustments, the decision was made to vary enemy waves, making them easier when close to defeat and more challenging when nearing victory. To achieve this, I made custom containers and even a custom editor for the wave manager, facilitating dynamic adjustments based on the game's state.

Spawn manager editor.
Spawn manager editor.
Spawn Manager editor with waves expanded for editing.
Spawn Manager editor with waves expanded for editing.

 

As depicted above, the waves were segmented based on maximum health of the win condition, in percentages. I implemented a feature within the custom editor to ensure no two health percentages overlapped. To organize enemy prefabs for each wave, I devised a ScriptableObject container holding an array of enemy prefabs. This streamlined the process for designers to arrange, reuse, and swiftly interchange various wave types during testing. Additionally, I designed a container class to simplify setup and retain setup information alongside the wave data.

Enemies walking on path

Additional Work

In addition to the main features I developed I also made a few smaller things.

  • Introduced an enemy manager, serving as a foundational framework for managing enemy components and their interactions.

  • Developed a reusable HealthSystem comprising both a Monobehaviour and a ScriptableObject component.

  • Implemented a StatusEffect system, incorporating an IEffectable interface for classes susceptible to effects and a StatusEffect base for derived effects.

© 2024 by Johan Brandt

bottom of page