Play as an officer in charge of keeping the injured alive amidst a futuristic war, where life and death are a roll of a dice. In this UI-based management sim, carefully assess each day's incoming patients and assign care providers to keep as many alive while balancing the hospital morale.
Role: Programmer
Company: Personal Project - GMTK 2022
Tools: Unity, FMOD, Jetbrains Rider, Github
Plug-ins: Cinemachine, FMOD for Unity
Duration: Aug 2022
Team Size: 6
Genre: Action
Platform: PC
Project Status: Bug Fixes
I worked alongside another programmer to develop a finite state machine that would handle setting and changing the current perspective of the player in relation to the 3D world. The state machine uses enums to determine the active state, which then determines the player's current perspective, and the active behavior of the FSM.
To call the finite state machine's state switching function in GameEventListeners, the enums that defined the three potential perspectives had to be encapsulated within a scriptable object. This scriptable object could then be added as an input and read by the SwitchStates function, which could have its value parsed through a switch statement. This would allow for the easy updating of the state in response to external signals.
public void SwitchStates(Perspective switchToPerspective)
{
// Return out of the switch state call is to the same state.
if (_currentState.Perspective == switchToPerspective.PerspectiveEnum)
{
Debug.Log("Called Same State A");
return;
}
PlayerBaseState newState;
switch (switchToPerspective.PerspectiveEnum)
{
case (PerspectiveEnum.FRONT):
newState = _frontViewState;
break;
case (PerspectiveEnum.SIDE):
newState = _sideViewState;
break;
case (PerspectiveEnum.TOP):
newState = _topViewState;
break;
default:
newState = _currentState;
Debug.LogError("Perspective not recognized by " +
"PlayerStateMachine.SwitchStates().");
break;
}
_currentState.ExitState();
_currentState = newState;
_currentState.EnterState();
Debug.Log(_currentState.Perspective.ToString());
}
public void OnClickGameWorld()
{
_currentState.OnClickGameWorld();
}
private void Update()
{
_currentState.UpdateState();
}
This abstract class defined all of the vital functions shared between all states. Since the states all existed effectively as a part of the finite state machine, the direct reference to it is not considered improper dependency.
public abstract class PlayerBaseState
{
public PerspectiveEnum Perspective;
protected PlayerStateMachine Context;
public abstract void EnterState();
public abstract void ExitState();
public abstract void OnClickGameWorld();
public abstract void UpdateState();
}
With the three views split between the programmers on the team, I took on the front view gameplay section. Players observing the battlefield from the front would be able to control a railgun that could shoot through the entire lane. To create such a gameplay system required three major considerations - the cooldown, the aiming, and the actual firing sequence. By separating these individual parts out and carefully considering how how data flow should be handled, I was able to create a highly modular system that leveraged Unity's scriptable objects as data containers.
When thinking about how to handle cooldown, my first thoughts went into imagining it through the lens of an I/O module.
This immediately narrowed the system down into two major signals - an input was needed to tell the cooldown to reset itself and an output needed to be sent when the cooldown elapsed. What was sending the input or receiving these output was unimportant to the design and the way I wanted to design the system, I wanted whatever what was receiving the output or sending the input to be entirely blind to the cooldown system as well.
To achieve this, I leveraged scriptable object to store data and communicate between MonoBehaviors. This was done primarily through the observer pattern. The input of the cooldown system, the signal to reset, was handled through a listener subscribed to a scriptable object. Whatever system sent the signal to the subscribed scriptable object was hidden from the listener but in response to it, the listener would tell the cooldown controller to reset the countdown. Similarly, the output that the cooldown system produced - the signal that the cooldown had elapsed - was sent through a scriptable object. Any external system could use a listener to subscribe to the cooldown system's scriptable object and call a function in response. This allowed them to update themselves when the cooldown elapsed without needing a reference to the cooldown system or even knowing that one existed. Through this system of scriptable objects and listeners, I was able to create a wholly isolated cooldown system that would not be dependent or be depended on by anything else in the game.
An implementation of a scriptable object used to store a float value. This could be used to send data between MonoBehaviors, such as a controller and a display, while hiding .
/// <summary>
/// Delegate to signal that a ScriptableObjectVariable has been updated.
/// </summary>
public delegate void VariableUpdatedDelegate();
public abstract class FloatVariable : ScriptableObject
{
/// <summary>
/// Delegate to signal that this ScriptableObjectVariable has been updated.
/// </summary>
public VariableUpdatedDelegate VariableUpdated;
/// <summary>
/// The value of this variable.
/// </summary>
[SerializeField] private float value;
/// <summary>
/// The value of this variable. Invokes VariableUpdated when set.
/// </summary>
public float Value
{
get
{
return this.value;
}
set
{
this.value = value;
VariableUpdated?.Invoke();
}
}
}
The logic that controls the cooldown counter. Refresh Cooldown is called by an GameEventListener in response to a Game Event being raised. This listener is generic and separate from this controller script. The output of the controller is also a GameEvent - labelled cooldownFinished - and can be subscribed to by any other system via a GameEventListener. The data for the cooldown and whether the cooldown has elapsed are saved as scriptable objects to allow for potential UI display.
public class CooldownControl : MonoBehaviour
{
[SerializeField] private FloatVariable cooldownBase;
[SerializeField] private FloatVariable cooldownActual;
[SerializeField] private GameEvent cooldownFinished;
[SerializeField] private BoolVariable cooldownElapsed;
private void Start()
{
RefreshCooldown();
}
public void RefreshCooldown()
{
cooldownActual.Value = cooldownBase.Value;
}
// Update is called once per frame
void Update()
{
if (cooldownActual.Value > 0)
{
if (!cooldownElapsed.Value)
{
cooldownElapsed.Value = true;
cooldownFinished.Raise();
}
return;
}
cooldownElapsed.Value = false;
cooldownActual.Value -= Time.deltaTime*0.6f;
}
}
Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.
Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.
Text
private void Start()
{
EnableScreen();
}
private void OnEnable()
{
cooldownStart.VariableUpdated += EnableScreen;
}
private void OnDisable()
{
cooldownStart.VariableUpdated -= EnableScreen;
}
private void EnableScreen()
{
aimingButton.interactable = cooldownStart.Value;
}
Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.
Text
public void SetLane()
{
savePoint = transform.position;
Debug.Log("Set Lane");
transform.position = new Vector3(16*(railgunLane.Value) + 8, transform.position.y,transform.position.z);
railgunAnimator.Play("RailgunShot");
// transform.position = savePoint;
}
Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.
public void SetLane()
{
savePoint = transform.position;
Debug.Log("Set Lane");
transform.position = new Vector3(16*(railgunLane.Value) + 8, transform.position.y,transform.position.z);
railgunAnimator.Play("RailgunShot");
// transform.position = savePoint;
}
In the animation, the box collider is enabled when the laser is the widest and then turned off at the end of the animation.