About 10 days ago, around the time of my last post in this series, I laid out my plan for how I would complete the basic gameplay prototype of my game. It would be as barebones as possible, only including the features absolutely necessary to test the core gameplay concept.
To recap, the vision for the game is an online multiplayer, synchronous turn-based fighting game for iOS and Web with colorful visuals, uniquely stylized characters, and social features such as player profiles, leaderboards, and character customization. The gameplay prototype milestone just includes the "online" and the "turn-based" features. That means no animations, all default UI assets, placeholder character models, and no sound anywhere in the game.
Last night, I marked that milestone complete and sent a small group of my gaming friends the first build. After so much testing in the Unity editor playing against myself, I got to watch the very first instance of my game being played by other people.
The rounds last too long. There were some instances where combat didn't properly resolve. There's no move history being displayed for either player so at times the players felt like they were just "mashing". Without the animations and visual/audio flavor the game feels slow.
But, the reception was good! I was really nervous to share this first prototype, being as barebones as it is, but I am so glad that I did. I recorded all of the feedback and have distilled it into issues and tasks on my GitHub Project, which will help me to iterate over the design, mechanics and tuning of the game. Overall, the prototype did its job--it allowed me to see if the core gameplay loop is any fun, and based on the feedback, it is! When the play testers found themselves getting pulled into the "one more game" vortex, I knew that was a good sign.
In the final push to get the protoype completed, I definitely sacrificed some code quality to get there. "Oh I need to track some piece of state? Throw it in the GameData
class," was said multiple times. I have set aside some time to refactor a large chunk of the code pushed out for the prototype in order to make it more maintainable, testable, and readable.
With that said, there are elements of the code base that I am proud of. Huge shout outs to @GameDevGuide and their content on YouTube for inspiration for many of these.
Progress Bar
I looked for opportunities to create reusable components for the UI, and one such example is the ProgressBar
component.
[ExecuteInEditMode()]
public class ProgressBar : MonoBehaviour
{
public float maximum;
public float current;
public Image mask;
public Image fill;
public Direction fillDirection;
public Color fillColor;
public enum Direction : int
{
left = Image.OriginHorizontal.Left,
right = Image.OriginHorizontal.Right
}
void Update()
{
GetCurrentFill();
}
void GetCurrentFill()
{
float fillAmount = current / maximum;
mask.fillAmount = fillAmount;
mask.fillOrigin = (int) fillDirection;
fill.color = fillColor;
}
public void SetCurrent(float newValue)
{
current = newValue;
}
public void SetMaximum(float newValue)
{
maximum = newValue;
}
}
This component is being used to manage multiple elements of the gameplay UI...
...and its interface allows it to be controlled in both the Unity editor and script!
State Machine
I created a state machine that utilizes an abstract GameBaseState
class and a strategy pattern to manage the different stages of the game. Now I can execute functions of the game system at specific times by calling them in either the EnterState()
, UpdateState()
, or ExitState()
method of their corresponding concrete state object.
Abstract class for a game state.
public abstract class GameBaseState
{
protected GameStateMachine _context;
protected GameStateFactory _factory;
public GameBaseState(GameStateMachine currentContext, GameStateFactory gameStateFactory)
{
_context = currentContext;
_factory = gameStateFactory;
}
public abstract void EnterState();
public abstract void UpdateState();
public abstract void ExitState();
public abstract void CheckSwitchStates();
protected void SwitchState(GameBaseState newState)
{
ExitState();
newState.EnterState();
_context.CurrentState = newState;
Debug.Log($"New State: {newState}");
}
public override string ToString()
{
return this.GetType().Name;
}
}
Here is a very simple example of a concrete implementation of the abstract GameBaseState
.
public class GameNotReadyState : GameBaseState
{
public GameNotReadyState(GameStateMachine currentContext, GameStateFactory gameStateFactory) : base(currentContext, gameStateFactory)
{
}
public override void CheckSwitchStates()
{
if (_context.AllPlayersLoaded)
{
SwitchState(_factory.Start());
}
}
public override void EnterState() {}
public override void ExitState() {}
public override void UpdateState()
{
CheckSwitchStates();
}
}
This is the machine that utilizes Unity MonoBehavior
/NetworkBehaviour
lifecycle methods and a strategy pattern to manage and execute the current state's methods.
public class GameStateMachine : NetworkBehaviour
{
//...
private GameBaseState _currentState;
private GameStateFactory _states;
public override void OnNetworkSpawn()
{
if (!IsServer) return;
_states = new GameStateFactory(this);
_currentState = _states.NotReady();
_currentState.EnterState();
}
void Update()
{
if (!IsServer) return;
_currentState.UpdateState();
}
}
Lightweight State Data
I'm also proud of how I minimized storage of the usable moves for each player using a single byte and bit masking. Part of my refactoring efforts I mentioned above will be to encapsulate all of the bit arithmatic into its own class. For now, though, I'm able to store a single byte and communicate it between host and client with very lightweight packets, and update the state of a specific move just by flipping a bit.
This is the enum that represents a move's type.
public class CharacterMove : ScriptableObject
{
//...
public enum Type : byte
{
LightAttack = 16,
HeavyAttack = 8,
Parry = 4,
Grab = 2,
Special = 1
}
//...
}
Here is an example of how the specific bit for a move can be modified to affect state.
public class GameData : NetworkBehaviour
{
//These get initialized to 0b11110 when a player object is spawned.
private NetworkVariable<byte> _usableMoveListPlayer1 = new(0);
private NetworkVariable<byte> _usableMoveListPlayer2 = new(0);
//...
public void CheckShouldSpecialBeEnabled()
{
if (_specialMeterPlayer1.Value >= 100f)
{
_usableMoveListPlayer1.Value |= (byte)CharacterMove.Type.Special;
}
if (_specialMeterPlayer2.Value >= 100f)
{
_usableMoveListPlayer2.Value |= (byte)CharacterMove.Type.Special;
}
}
//...
}
...and how the UIManager can subscribe to and update interactable buttons based on that state.
public class GameUIManager : NetworkBehaviour
{
//...
public override void OnNetworkSpawn()
{
_data.UsableMoveListPlayer1.OnValueChanged += UpdateUsableMoveButtons;
//...
}
public override void OnNetworkDespawn()
{
_data.UsableMoveListPlayer1.OnValueChanged -= UpdateUsableMoveButtons;
//...
}
private void UpdateUsableMoveButtons(byte previousValue, byte newValue)
{
foreach (CharacterMove.Type type in Enum.GetValues(typeof(CharacterMove.Type)))
{
var button = _playerControls.GetButtonByType(type).Button
button.interactable = (newValue & (byte)type) == (byte)type;
}
}
There's a lot more to do. More play testing, balancing damage/health values and round duration, character passive and special design tweaks, debugging, refactoring--not to mention my next major milestone: Look and Feel, First Pass. The initial positive feedback to the core gameplay has reassured me that I'm heading in a good direction with my design choices, and with v0.1.0 behind me, now I'm looking ahead to v0.2.0!
If you are interested in seeing my progress, the project can be found here.
Top comments (0)