Balance

Unity

VR

C#

This is my ongoing part-time coop as a research assistant implementing the gamified aspect of an astronaut balance testing VR program.

My role for this project is as a gameplay programmer


Four Square State Machine

Created a state machine that would represent the FourSquare game. This was then used to create this game and allows it to reset easily.

File IO

Since this is a research project we need a convenient way to store data across all games in reliable and easy to understand method. The following whiteboard is my draft of how I will implement this process.

FilIO has custom data structures for convivence and this script works directly with the json files stored locally. DataStoreLoad (DSL) is the main script developers will edit when adding a new game as it is where information is turned into FileIO’s data structures before being sent there. Above are game managers which use data how they choose fit.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class TempleGameManager : MonoBehaviour
{
    [Header("Overal Game Management")]
    [SerializeField] TempleGameStates gameState = TempleGameStates.CHOOSE_GAME_MODE;
    [SerializeField] Transform playerHead;
    [SerializeField] Transform playerLeft;
    [SerializeField] Transform playerRight;
    [Space]
    [SerializeField] GameObject instructionCanvas;

    [Header("Four Squares")]
    [SerializeField] FourSquareStates fourSquareState = FourSquareStates.GENERATE_PATTERNS;
    [Tooltip("How many squares will the player need to travel to before the round ends")]
    [SerializeField] int patternSize;
    [SerializeField] List<Tile> tiles;
    [SerializeField] Vector3 tileSize;
    [Tooltip("Used to instantiate barriers")]
    [SerializeField] GameObject barrierObj;
    [SerializeField] List<Cube_Transform> barriers;
    [SerializeField] float pointsGainSuccess;
    [SerializeField] float pointsLossCollision;

    [Space]
    [SerializeField] int currentDifficulty; 
    [SerializeField] List<DifficultySettings> FS_Difficulties;

    //[Header("Pose Copy")]
    //[SerializeField]

    private void Start()
    {
        // Setup Foursquares 
        barrierTransforms = new List<Transform>();
        for (int i = 0; i < barriers.Count; i++)
        {
            barrierTransforms.Add(Instantiate(barrierObj, this.transform.position, Quaternion.identity).transform);
            barrierObj.SetActive(false);
        }
        UpdateBarrierTransforms();
    }

    void Update()
    {
        TempleSM();
    }

    #region GAME_MANAGER

    public void SetGameMode(int gameMode)
    {
        gameState = (TempleGameStates)gameMode;

        // Any intitialization 
        switch (gameState)
        {
            case TempleGameStates.CHOOSE_GAME_MODE:
                break;
            case TempleGameStates.POSE_MATCH:
                break;
            case TempleGameStates.FOUR_SQUARE:
                InitializeFourSquare();
                break;
            case TempleGameStates.DISPLAY_SCORE:
                break;
        }
    }

    /// <summary>
    /// State machine that runs the temple game modes 
    /// </summary>
    void TempleSM() 
    {
        switch (gameState)
        {
            case TempleGameStates.CHOOSE_GAME_MODE:
                ChooseGameMode();
                break;
            case TempleGameStates.POSE_MATCH:

                break;
            case TempleGameStates.FOUR_SQUARE:
                FourSquareSM();
                break;
            case TempleGameStates.DISPLAY_SCORE:
                DisplayScore();
                break;
        }

        instructionCanvas.SetActive(gameState == TempleGameStates.CHOOSE_GAME_MODE);
    }

    /// <summary>
    /// Lets the user choose what game to play 
    /// </summary>
    void ChooseGameMode()
    {
        // Temporary 
        //gameState = TempleGameStates.FOUR_SQUARE;

        // Game mode is decided by button press 
    }

    void DisplayScore()
    {
        gameState = TempleGameStates.CHOOSE_GAME_MODE;
    }

    /// <summary>
    /// Changes the visuals around the player to show the score of the
    /// desired gamemode
    /// </summary>
    /// <param name="game"></param>
    void BeginDisplayScore(GameModes game) { } 
   

    enum GameModes
    {
        POSE_MATCH,
        FOUR_SQUARE
    }

    #endregion

    #region FOUR_SQUARES


    // Arr of indexes each refer to differnt square 
    private int[] pattern;
    private int indexInPattern; // Index in pattern arr
    // Selected tile 
    private int currentTile;

    // Recorded data 
    private float score = 0.0f;
    private int collisions = 0;

    // The generated transforms 
    private List<Transform> barrierTransforms;

    /// <summary>
    /// Sets up this game 
    /// </summary>
    void InitializeFourSquare()
    {
        fourSquareState = FourSquareStates.GENERATE_PATTERNS;
        UpdateBarrierTransforms();

        foreach (Transform t in barrierTransforms)
        {
            t.gameObject.SetActive(true);
        }
    }

    /// <summary>
    /// Manages the states of the Four Square game-mode 
    /// </summary>
    void FourSquareSM() 
    { 
        switch (fourSquareState)
        {
            case FourSquareStates.GENERATE_PATTERNS:
                GeneratePatterns();
                break;
            case FourSquareStates.DISPLAY_PATTERN:
                DisplayPattern();
                break;
            case FourSquareStates.PLAY:
                PlayState();
                break;
            case FourSquareStates.END_GAME:
                FourSquareEndGame();
                break;
        }
    }

    /// <summary>
    /// Generate patterns state
    /// </summary>
    private void GeneratePatterns()
    {
        pattern = GeneratePattern();
        indexInPattern = 0;
        currentTile = pattern[indexInPattern];

        fourSquareState = FourSquareStates.DISPLAY_PATTERN;
    }


    /// <summary>
    /// Creates an array of indexes that each represent one of the squares that the player must step to. 
    /// There cannot be the same two indexes in a row. 
    /// </summary>
    /// <returns></returns>
    private int[] GeneratePattern()
    {
        int[] pattern = new int[patternSize];
        int previous = -1;

        for (int i = 0; i < patternSize; i++)
        {
            int current;
            
            // Make sure not tile that player is currently standing on 
            if (i == 0)
            {
                int playerCurrentTile = -1;

                // Find current tile player head is in 
                for (int t = 0; t < tiles.Count; t++)
                {
                    print("in tiles search " + InTile(playerHead, t));
                    if (InTile(playerHead, t))
                    {
                        playerCurrentTile = t;
                        break;
                    }
                }


                // Don't continue if not in any tile 
                if(playerCurrentTile != -1)
                {

                    // Continue until current is not the tile 
                    // the player is in 
                    do
                    {
                        // 0 to 4 each represents a square index 
                        current = UnityEngine.Random.Range(0, 4);

                    } while (current == playerCurrentTile);

                    // Set values 
                    pattern[i] = current;
                    previous = current;

                    continue;
                }
                
            }


            // Make sure pattern does not repeat 
            do
            {
                // 0 to 4 each represents a square index 
                current = UnityEngine.Random.Range(0, 4);

            } while (current == previous);

            // Set values 
            pattern[i] = current;
            previous = current;

        }

        return pattern;
    }

    /// <summary>
    /// Shows the current square that the player must travel to. 
    /// </summary>
    private void DisplayPattern() 
    {
        // Send pattern to console 
        string patternStr = "";
        for (int i = 0; i < patternSize; i++)
        {
            patternStr += pattern[i].ToString();
        }
        print("Current pattern: " + patternStr);

        // Reset 
        for (int i = 0; i < tiles.Count; i++)
        {
            tiles[i].SetSelection(i == currentTile ? FS_Square.FSSquareStates.TARGET : FS_Square.FSSquareStates.NOT_TARGET);
        }

        fourSquareState = FourSquareStates.PLAY;
    }

    /// <summary>
    /// Dictates when the player has reached the desired square 
    /// </summary>
    private void PlayState()
    {
        // TODO: Figure out why we need to check if the round is complete twice...


        // Playstate Management 
        if (IsRoundComplete())
        {
            fourSquareState = FourSquareStates.END_GAME;
            return;
        }

        if (PlayerInTarget())
        {
            score += pointsGainSuccess;
            indexInPattern++;

            // Reset all barriers to become dangerous again 
            foreach (Transform t in barrierTransforms)
            {
                t.GetComponent<Barrier>().SetDanger(true);
            }

            if (IsRoundComplete())
            {
                fourSquareState = FourSquareStates.END_GAME;
                return;
            }

            currentTile = pattern[indexInPattern];

            fourSquareState = FourSquareStates.DISPLAY_PATTERN;
        }



    }

    /// <summary>
    /// Changes how the game is visualized 
    /// </summary>
    private void UpdateBarrierTransforms()
    {
        // Barrier Height 
        DifficultySettings settings = FS_Difficulties[currentDifficulty];
        for (int i = 0; i < barriers.Count; i++)
        {
            float height = UnityEngine.Random.Range(settings.heightMin, settings.heightMax);

            Cube_Transform transform = barriers[i];
            barrierTransforms[i].position = this.transform.position + transform.position + Vector3.up * height / 2.0f;
            barrierTransforms[i].localScale = new Vector3(transform.scale.x, settings.heightMin, transform.scale.y);
        }
    }

    /// <summary>
    /// Final state of this game which transitions from four square to display score 
    /// </summary>
    private void FourSquareEndGame()
    {
        // TODO: Write data to text file 
        print("Final Score: " + score);
        print("Total collision: " + collisions);

        // Reset to default visual 
        foreach(Tile tile in tiles)
        {
            tile.tileObj.SetTargetVisual(FS_Square.FSSquareStates.NOT_IN_PLAY);
        }

        // Turn off barriers 
        foreach (Transform t in barrierTransforms)
        {
            t.gameObject.SetActive(false);
        }

        score = 0.0f;
        collisions = 0;
        currentTile = 0;
        indexInPattern = 0;

        gameState = TempleGameStates.DISPLAY_SCORE;
    }

    /// <summary>
    /// Detects whether the player is fully within the desired target. 
    /// This is done by checking if the feet, hands, and head occupy the desired area.
    /// </summary>
    /// <returns></returns>
    private bool PlayerInTarget()
    {
        int tile = pattern[indexInPattern];
        return InTile(playerHead, tile) && InTile(playerLeft, tile) && InTile(playerRight, tile);
    }

    /// <summary>
    /// Used to check if a transform is within range of a given tile index 
    /// </summary>
    /// <param name="transform"></param>
    /// <param name="tileIndex"></param>
    /// <returns></returns>
    private bool InTile(Transform transform, int tileIndex)
    {
        Vector3 tileCurr = this.transform.position + tiles[tileIndex].tilePos;
        Vector3 halfSize = tileSize / 2.0f;

        bool aboveMin =
                transform.position.x >= tileCurr.x - halfSize.x &&
                transform.position.z >= tileCurr.z - halfSize.z;

        bool belowMax =
            transform.position.x <= tileCurr.x + halfSize.x &&
            transform.position.z <= tileCurr.z + halfSize.z;

        return aboveMin && belowMax;
    }

    /// <summary>
    /// Returns true if the the entire pattern has been completed 
    /// </summary>
    /// <returns></returns>
    private bool IsRoundComplete()
    {
        return indexInPattern >= patternSize;
    }

    /// <summary>
    /// Call if the player has made collision with a barrier 
    /// </summary>
    public void TakeCollision()
    {
        score -= pointsLossCollision;
        collisions++;
    }

    enum FourSquareStates
    {
        GENERATE_PATTERNS,  // Setup for the game round 
        DISPLAY_PATTERN,    // Indicates the current square 
        PLAY,               // Grades player score and waits for their input 
        END_GAME            // Cleanup and send to display score 
    }

    [System.Serializable]
    private class Tile
    {
        [SerializeField] public Vector3 tilePos;
        [SerializeField] public FS_Square tileObj;


        /// <summary>
        /// Update the visual of this tile to represet whether it is 
        /// the current target tile or not 
        /// </summary>
        /// <param name="selection"></param>
        public void SetSelection(FS_Square.FSSquareStates state)
        {
            tileObj.SetTargetVisual(state);
        }

    }

    [System.Serializable]
    private class Cube_Transform
    {
        [SerializeField] public Vector3 position;
        [Tooltip("Only can decide x and z axis as height is done by difficulty")]
        [SerializeField] public Vector2 scale;
    }

    [System.Serializable] 
    private class DifficultySettings
    {
        [SerializeField] public string Name;
        [SerializeField] public float heightMax;
        [SerializeField] public float heightMin;

        [Tooltip("Each wall is random in height")]
        [SerializeField] public bool variableHeights;
    }


    #endregion

    #region POSE_COPY

    /// <summary>
    /// Randomly chooses a pose from a list of poses that is not the same as the previous pose.
    /// </summary>
    /// <returns></returns>
    Pose SelectPose()
    {
        return null;
    }

    /// <summary>
    /// Animated and visualizes the pose that was chosen. 
    /// Indicates what the general pose the player should make.
    /// </summary>
    void PlaySelection()
    {

    }

    /// <summary>
    /// Visualizes how close the player is to matching the pose. 
    /// Changes the hands and feet of intstructor by having a green check, yellow minus, or red cross. 
    /// </summary>
    void DisplayPoseCloseness()
    {

    }

    /// <summary>
    /// Resets the display so that there is no more pose. A cleanup function
    /// </summary>
    void ResetPoseGame()
    {

    }

    [System.Serializable]
    private class Pose
    {
        [SerializeField] private Vector3 hand_L, hand_R, foot_L, foot_R;

        /// <summary>
        /// Passes in the current player’s limb positions and returns an array of enums that represent how close they are to matching the pose. 
        /// </summary>
        /// <returns>Array returned is of size 4 and has ratings in order from {hand_L, hand_R, foot_L, foot_R}</returns>
        MatchLevel[] GetPoseMatchLevels(Vector3 hand_L, Vector3 hand_R, Vector3 foot_L, Vector3 foot_R)
        {
            return null;
        }
    }

    enum MatchLevel
    { 
        NOT_MATCHED,
        CLOSE,
        MATCHED
    }


    #endregion


    private void OnDrawGizmosSelected()
    {
        // Represent tiles areas 
        for (int i = 0; i < tiles.Count; i++)
        {
            if(i == currentTile)
            {
                Gizmos.color = Color.black;
            }
            else
            {
                Gizmos.color = Color.red;
            }

            //Gizmos.DrawCube()
            Gizmos.DrawWireCube(this.transform.position + tiles[i].tilePos, tileSize);
        }

        Gizmos.color = Color.magenta;
        foreach (Cube_Transform transform in barriers)
        {
            DifficultySettings settings = FS_Difficulties[currentDifficulty];

            if(Application.isPlaying)
            {
                Gizmos.DrawWireCube(
                this.transform.position + transform.position + Vector3.up * settings.heightMin / 2.0f,
                new Vector3(transform.scale.x, settings.heightMin, transform.scale.y)
                );
            }
            else
            {
                Gizmos.DrawCube(
                this.transform.position + transform.position + Vector3.up * settings.heightMin / 2.0f,
                new Vector3(transform.scale.x, settings.heightMin, transform.scale.y)
                );
            }
        }
    }
}

Temple GM

The following block of text is snapshot of what I did for the project. Since the repo is not public the rest of this page is the code for that aspect of the game. There is nothing below that of interest so only continue if you wish to take a look at my code.