David Liu

Hatmancers

About the Game

Hatmancers is a fast-spaced, multiplayer third-person spellslinger made to be playable for all ages. The simplicity of the mechanics gives accessibility to a wide audience, allowing players to easily jump into the action without having to learn complex controls or systems. The game is made to be played in quick rounds, allowing the game to be started and stopped in convenient, short intervals. Filled with different kinds of magic abilities imbued into unique hats, Hatmances is sure to bring tons of action-packed fun to any group of friends.

Developed from August 2019 to December 2019.

Play the game here!

Role in Development

  • Managers: Michael Berger, Nathan Glick
  • Designer: Nathan Glick
  • Programmers: David Liu, Nathan Glick
  • Modelers: Surya Ajayakumar, Abhishek Chandrashekhar Panhale

My main role in development for this project was Gameplay Programming. I developed the player controls as well as handling multiplayer functionality in the game. The game supports compatibility for both Xbox and Playstation controllers in conjunction to keyboard controls. I also assisted in fixing bugs and tweaking the game for a balanced experience. My biggest struggle during this project was detecting multiplayer input across different controllers. Unity's control system at the time had interchangeable axes and buttons across both Xbox and Playstation controllers. To get around this, I had the menu detect a starting input from either keyboard, Xbox, or Playstation controller and assigned it to a player.

Script Examples

PlayerController.csCameraMouse.csManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
/// Handles all inputs from the player. Currently consists of:
///     - Movement
///     - Jumping
/// Authors: Abhi, David, Michael
/// Code Resource: https://forum.unity.com/threads/proper-velocity-based-movement-101.462598/
public class PlayerController : MonoBehaviour
{
    // Modifiable Attributes
    public int playerNum;
    public Transform head;
    public float speed;
    public float rotationSpeed;
    public float jumpSpeed; // Initial jump velocity
    public float jumpHoldTime; // Time the button can be held down to add force into the air
    public float gravityForce; // Force of gravity weighing down in air

    //Pause Menu Variables
    [SerializeField]
    private Manager manager;

    // Rigidbody for physics
    private Rigidbody body;

    // Store CameraMouse to access orbit dampening
    private CameraMouse cameraMouse;

    // Storing WizardAnimScript to control animations
    private WizardAnimScript animScript;

    // Stored values for axes
    private float vertical;
    private float horizontal;

    // Timers
    private float jumpTimer;

    // Flags
    public bool grounded = false;
    public bool canMove = true;

    // Start is called before the first frame update
    void Start()
    {
        jumpTimer = 0;
        body = GetComponent();
        cameraMouse = head.gameObject.GetComponentInChildren();
        animScript = this.gameObject.GetComponentInChildren();
        manager = GameObject.Find("Manager").GetComponent();
    }

    // Update is called once per frame
    void Update()
    {

        // If player presses Escape button/Start button
        if (Input.GetAxis("STA" + playerNum) != 0)
        {
            if (!Manager.isGamePaused && !Manager.resumingGame)
            {
                manager.PauseGame();
            }
            else if (Manager.resumingGame)
            {
                Manager.resumingGame = false;
            }
        }
    }

    // Update is called once per frame
    void LateUpdate()
    {
        // If the game is paused, stop scene activities
        if (Manager.isGamePaused)
        {
            return;
        }

        if (!canMove) return;

        // Check if not on ground
        if (Physics.Raycast(transform.position, -transform.up, 0.5f))
        {
            grounded = true;
        }
        else
        {
            grounded = false;
        }

        // Get axes values
        vertical = Input.GetAxis("LSV" + playerNum);
        horizontal = Input.GetAxis("LSH" + playerNum);

        // Check if player wants to jump and can jump
        if (Input.GetAxis("A" + playerNum) > 0 && grounded)
        {
            // Add force to simulate a jump
            body.AddForce(Vector3.up * jumpSpeed, ForceMode.Force);
            jumpTimer = 0;
        }
        else if (Input.GetAxis("A" + playerNum) != 0 && !grounded && jumpTimer < jumpHoldTime)
        {
            body.AddForce(Vector3.up * jumpSpeed, ForceMode.Force);
            jumpTimer += Time.deltaTime;
        }
        else if (Input.GetAxis("A" + playerNum) == 0 && !grounded)
        {
            // Set counter force
            body.AddForce(Vector3.down * gravityForce, ForceMode.Force);
        }
        else if (!grounded)
        {
            body.AddForce(Vector3.down * (gravityForce / 2f), ForceMode.Force);
        }

        // Add more speed only if it hasn't reached max speed
        if (body.velocity.x < speed)
            body.AddForce(transform.forward * vertical * speed, ForceMode.Force);
        if (body.velocity.z < speed)
            body.AddForce(transform.right * horizontal * speed, ForceMode.Force);

        // Project velocity onto the ground
        body.velocity = Vector3.ProjectOnPlane(body.velocity, Terrain.activeTerrain.terrainData.GetInterpolatedNormal(transform.position.x, transform.position.z));

        // Deadzone for velocity
        if (body.velocity.magnitude < 0.1)
        {
            body.velocity = Vector3.zero;
            body.angularVelocity = Vector3.zero;
        }

        // Set horizontal rotation of the body to the horizontal rotation of the head
        transform.rotation = Quaternion.Euler(transform.rotation.y, cameraMouse.localRotation.x, 0f);

        /*  ======================
            ===== ANIMATIONS =====
            ======================
        */

        // IF the player is grounded & the player is NOT moving...
        if (grounded && vertical == 0 && horizontal == 0)
        {
            // Playing the idle animation
            animScript.Idle();
        }

        // IF the player is grounded & the player is moving...
        if (grounded && (vertical != 0f || horizontal != 0f))
        {
            // Play the running animation
            animScript.Run();
        }

        // IF the player presses the shoot button...
        if (Input.GetAxis("LT" + playerNum) > 0 || Input.GetAxis("RT" + playerNum) > 0)
        {
            // Play the shooting animation
            animScript.Shoot();
        }
    }

    /// Gets the player number value.
    public int GetPlayerNum()
    {
        return playerNum;
    }
}