Just Another Space Shooter

About the game

Just Another Space Shooter (JASS) is a solo project I made during the Game Design & Technology minor at Fontys. It was made in Unity. Players control a single gunship which they can move in any direction across the screen. The goal is to stay alive for as long as the player can manage, and to kill as many enemies as they can. To that end, the player can use three different weapons. These weapons can be unlocked and upgraded by spending points earned by killing enemies and levelling up. To get a passing grade for this assignment, several subjects had to be present in the project in some way: AI, API integration, efficient level creation, multiplatform support and persistence.

AI

I put most of my focus in creating the AI, because I wanted to learn more about how to make Finite State Machines. To give the Finite State Machine a visual component, I used this source to integrate Mecanim. Although I did make use of the code that compares Mecanim states to enum values, the State Machine itself was to simplistic for my taste. To create the State Machine, I used an example project. Firstly, I created an abstract FSMState class, which is inherited by the state classes. These classes define the behaviour the NPC should exhibit while that state is active, and contain a reference to the enum value of that state.
The main FiniteStateMachine script contains a list of all the states of the NPC. A check is performed in every Update to see which Mecanim state is currently active. If this state does not correspond to the currently active state script, the appropriate script is activated.
Using Mecanim as Finite State Machine has the advantage of not having to program the state transitions yourself. Because Mecanim is capable of storing variables and using these to check the conditions of a transition, transitions are all handled by Mecanim itself.

One of the enemies (called a drone) appears in large swarms and traces a path across the screen to block the player's movement. These paths are created with bezier splines (see my Vertigun page). The player's positions are periodically stored, and based on these positions an average position is calculated. The spline is then placed on this average position (so, if a player stays in the upper right corner, drone swarms will appear there as well). The bezier splines can be created by hand in the Unity editor and stored as a prefab. The game will make use of the newly created bezier spline simply by dragging the prefab into the appropriate inspector field.

Image 2

Persistence, API and multiplatform support

The game has a limited form of saving and loading. The game can be saved at any point in time, but only one saved game is stored at a time. All relevant data (wave number, scores, upgrades, etc.) are stored in a serialized object. Upon loading a save, the information stored in the object is read and used to change the game's state.

The game uses the GameJolt API to remotely store high scores and trophies. The game keeps track of how many enemies were killed, the number of waves the players has beaten and the score the player has earned (by killing enemies and levelling up). If the player is not logged into GaneJolt, the scores are stored via the PlayerPrefs. When the player reconnects to GameJolt, the PlayerPrefs scores and the GameJolt scores are compared, and the lower scores are overwritten. Trophies are only available while logged in, and are unlocked by, for example, not taking any damage for a certain amount of time or upgrading a weapon to a certain rank.

The game is playable on both Android and PC (download links below). To control the ship on Android, simply touch the screen where you want the ship to go. While touching the screen, the ship will also fire its weapons.


Download the game for Android or PC.

The Finite State Machine of one of the enemies in the game. In the top left is a list of variables stored by Mecanim. The blue transition arrow has the condition shown at the bottom of the image. When the enemy's Health reaches below 0, the Dead state becomes active.

public enum StateID
{
    None,
    ReachedScreenEdge,
    AttackingPlayer,
    MovingToPosition,
    InFormation,
    Dead
}

...


//Cache all the hashes of the States in our State Machine (case sensitive!)
foreach (StateID state in (StateID[])System.Enum.GetValues(typeof(StateID)))
{
    stateDictionary.Add(Animator.StringToHash("Base Layer." + state.ToString()), state);
}
public abstract class FSMState {
    // Controls behaviour of the npc
    public abstract void UpdateState();
    //Reset state variables
    public abstract void ResetState();
    //This state's ID
    public abstract StateID GetStateID();
}
public void UpdateStateMachine()
{
    StateID nextStateID = stateDictionary[stateMachine.GetCurrentAnimatorStateInfo(0).fullPathHash];
    if (nextStateID != currentStateID) //If state has changed
    {
        currentStateID = nextStateID;
        foreach (FSMState state in states) //Get state
        {
            if (state.GetStateID() == currentStateID)
            {
                if (currentState != null)
                {
                    currentState.ResetState();
                }
                currentState = state;
                break;
            }
        }
    }

    if (currentState != null)
    {
        currentState.UpdateState(); //update current state
    }
}