Livli

Duration:  2 Weeks

Team size: 3 programmers 3 2D artists

Language: C#

Engine: Unity

My role: Gameplay programmer,

Ui programmer

This was my first team project, and our goal was to make a 2D arcade game by combining two classic arcades. We based our game on Ice Climbers and Space Invaders.


The game was developed in Unity. Livli is a diagonal side scroller with fast paced gameplay. In the game you play as the cunning hunter Livli on the quest to hunt down one of the devious vampires who have kidnapped Livlis loved one.


Jumping up the mountain with tons of enemies trying to weigh you down and stop you from reaching your goal. Armed with only your crossbow and determination you must slowly make your way up to the mountain through different levels until you finally can hunt down the one who took Livlis loved one away.


The game has a classical high score system to make it possible for players to challenge each other. Our key words were to make it easy to grasp but hard to master.

My role in this project was gameplay programmer and UI programmer. I worked with the following scripts Character, PlayerController, HealthBar and a little bit on the EnemyController. I also participated with the UI and set up the health bar and parts of the menu that our 2D artist had made.

In our team we were 6 first year students at FutureGames, 3 programmers and 3 2D artists with different background knowledge. One of our programmers had a couple of years’ experiences in making games and for me who had no prior knowledge of programming at all, I found it educational to learn from someone else's experiences.


Livli was developed over two weeks. To better get an overview what everyone was working on we set up a Trello board and held daily stand ups. Our approach was to have a democratic system where we voted on the best solutions for the product, we also committed to a team contract which I think was suitable for our group.


We did the mistake of changing a major mechanic too far into the project, which was the scoring system, but we solved it with remaking the tasks and scrapping less important parts to create a larger timeframe.


Looking back, I am proud of how the game turned out even though it had its flaws such as mechanics being a bit edgy. All of us were on the same page that if we were to go back and change anything, we would have put less time in to planning around in circles and instead focusing on the main idea that we had from the start that would have made our game less over scoped.


This project was challenging at times, but I learned a lot about c#, Unity and this is the project where I learned most about working as a team because we had a huge variety of different personalities which made us learn how to communicate in an efficient yet respectful way. I would say that we had a good dynamic in our team mostly because we realized the importance of teamwork.

The programming part. 

For this project we started off with creating a class diagram to help us work as a team. It was my first class diagram that I ever worked with and I found it to be a helpful tool for the future.


We had it in an online document so that we could edit the colors of the text to easier get a better overview which variables that should be used and which functions that were done and were in progress.


We also divided tasks based on our prior knowledge and what we wanted to learn. We were all on the same page with what we could contribute with.

script from watching different tutorials and by getting help from a more experienced programmer.


Player movement

Since we chose to create a fast-paced game with arrow shooting mechanism. We wanted to make it easy for the player to move that is why we made it, so the player only had to focus on pressing the space button to get the character to jump.

Player Health

The player has 5 lives to reach the top. Since the game is fast paced, we chose to have a fast camera. If you either fall outside of the frame or get hit by an Enemy before shooting at it, you will lose 1 life. The game is over when the player has lost all 5 lives.

Enemies

The Enemies spawn randomly on the tiles and to get by them you must shoot an arrow by left clicking on the mouse. Since we only have two inputs which is the jumping mentioned above and the shooting this makes it easier to focus on shooting the targets.

The code

This was my first project and I had only been at Furturegames for about a month so i had almost no knowledge about code or game development.


I started to learn the basics of code and at that time I did not unerstand that I should follow the documentation that Unity provided.


Looking back today, that is something that would have helped me understand better how to construct it. 

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

public class PlayerController : Character
{
    [SerializeField] private float jumpForce = 0f;
    private bool isGrounded;
    Rigidbody2D rb;
    private Vector3 jump;
    [SerializeField] private LayerMask mask;
    int direction = 1;
    float lastFramePosY;
    float posY;
    bool falling = false;
    bool hasReleasedSpace;
    public int maxHealth;

    float timer = 0;
    protected override void Start()
    {
        base.Start();
        rb = GetComponent<Rigidbody2D>();
    }
    private void Update()
    {
        if (moveDirection != 0)
        {
            animator.SetBool("Moving", true);

            if (moveDirection < 0)
            {
                direction = 1;
            }
            else
            {
                direction = -1;
            }
            transform.localScale = Vector3.up + Vector3.forward + Vector3.right * direction;
        }
        else { animator.SetBool("Moving", false); }
    }
    private void FixedUpdate()
    {
        animator.SetFloat("Shoot", 0);
        posY = transform.position.y;

        PlayerInput();
        if (!isGrounded && Input.GetAxisRaw("Jump") != 1)
        {
            rb.AddForce(Vector3.down * 9);
        }
        if (Input.GetAxisRaw("Jump") == 0)
        {
            hasReleasedSpace = true;
        }
        if (posY < lastFramePosY && !falling)
        {
            animator.SetBool("Falling", true);
            animator.SetBool("Jump", false);
            animator.SetBool("Grounded", false);
            falling = true;
        }
        if (falling && Physics2D.OverlapCircle(transform.position + Vector3.down * 0.3f, 0.15f, mask))
        {
            isGrounded = true;
            animator.SetBool("Falling", false);
            animator.SetBool("Grounded", true);
            falling = false;
        }
        if (Input.GetAxisRaw("Fire1") != 0)
        {
            animator.SetFloat("Shoot", 1f);
            transform.localScale = Vector3.up + Vector3.forward + Vector3.right * -direction;
            animator.SetBool("DoneShooting", true);
        }
        else
        {
            animator.SetFloat("Shoot", 0f);
            animator.SetBool("DoneShooting", false);
        }
        lastFramePosY = posY;
    }
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.tag != "Platform")
        {
            hp = 0;
        }
        if (collision.gameObject.tag == "Checkpoint")
        {
            GameController gc = FindObjectOfType<GameController>();
            gc.SetCheckPoint();

        }
        if(collision.gameObject.tag == "Enemy")
        {
            FindObjectOfType<Character>().Damaged(damageAmount);
        }
    }
    private void PlayerInput()
    {

        moveDirection = Input.GetAxisRaw("Horizontal");

        transform.position += Vector3.right * moveDirection * Time.deltaTime * speed;
        if (Input.GetAxisRaw("Jump") == 1 && isGrounded && hasReleasedSpace)
        {
            Jump();
        }


    }

    private void Jump()
    {
        animator.SetBool("Jump", true);
        animator.SetBool("Grounded", false);

        rb.AddForce(Vector3.up * jumpForce);


        //rb.velocity = new Vector3(rb.velocity.x, jumpForce, rb.velocity.y);
        isGrounded = false;
        hasReleasedSpace = false;
    }
    public void HealPlayer(int healAmount)
    {
        hp += healAmount;

        if(hp > maxHealth)
        {
            hp = maxHealth;
        }
    }
    public int Hp
    {
        get
        {
           
            return hp;
        }
        set
        {
            hp = value;
        }
    }

    public override void Damaged(int damage)
    {
        hp -= damage;
    }
}

      	
      
    

This is the base class that PlayerController and EnemyController inherits from. This was pair programmed by me and another programmer.

      
      	
using UnityEngine;

public abstract class Character : MonoBehaviour
{
    [SerializeField]
    protected float speed = 10f;
    [SerializeField]
    protected int hp =1;
    protected float moveDirection = 1f;
    protected Animator animator;
    public int damageAmount = 1;
    protected virtual void Start()
    {
        animator = GetComponent<Animator>();
    }

    virtual protected void Move()
    {   

    }

    virtual protected void Shoot()
    {

    }

    virtual protected void Death()
    {

    }

    virtual public void Damaged(int damage)
    {

    }
}

      	
      
    

This is the "health bar" which is shown in the left corner gifs above.

      
      	
using UnityEngine;
using UnityEngine.UI;
public class HealthBar : MonoBehaviour
{
    [Serializefield]
    private int numOfHealth;
    private Image[] hearts;
    private Sprite fullHeart;
    private Sprite emptyHeart;
    public PlayerController controller;

    public void Start()
    {
        controller = FindObjectOfType<PlayerController>();
    }
    private void Update()
    {

        for (int i = 0; i <hearts.Length; i++)
        {
        if (controller.Hp > numOfHealth)
        {
            controller.Hp = numOfHealth;
        }
            if (i < controller.Hp)
            {
                hearts[i].sprite = fullHeart;
            }
            else
            {
                hearts[i].sprite = emptyHeart;
            }

            if (i < numOfHealth)
            {
                hearts[i].enabled = true;
            }
            else
            {
                hearts[i].enabled = false;
            }
        }
    }
}


      	
      
    

The EnemyController was my teammates script that I took some part in. The functions that i worked on was Attack, Damage and some of the checkAttackDistance.

      
      	
using System;
using UnityEngine;
public class EnemyController : Character
{
    Rigidbody2D rigidBody;
    PolygonCollider2D polygonCollider;
    CircleCollider2D circleCollider;

    GameController gc;

    private enum CurrentState { Idle, MovingLeft, MovingRight, Attack }

    [SerializeField] private LayerMask PlatformLayerMask;
    [SerializeField] private LayerMask PlayerLayerMask;


    [SerializeField] private int scoreValue;


    private CurrentState currentState;
    private CurrentState prevState;
    private float timer;
    [SerializeField] private float timerTick;

    private void Awake()
    {
        rigidBody = GetComponent<Rigidbody2D>();
        polygonCollider = GetComponent<PolygonCollider2D>();
        circleCollider = GetComponent<CircleCollider2D>();


        currentState = CurrentState.MovingRight;
        prevState = CurrentState.MovingLeft;

        timer = 0f;
        scoreValue = 100;
        hp = 3;

    }
    protected override void Start()
    {
        base.Start();
        animator.SetBool("Moving", true);
        if (gc == null)
        {
            gc = GameObject.Find("GameController").GetComponent<GameController>();
        }
    }
    private void Update()
    {
        Move();
        CheckAttackDistance();
    }


    protected override void Move()
    {
        //Wacky shit ya'll
        if (!animator.GetBool("Moving"))
        {
            prevState = currentState;
            currentState = CurrentState.Idle;

            timer += Time.deltaTime;
            animator.SetBool("Moving", false);
            if (timer >= timerTick)
            {
                animator.SetBool("Moving", true);
                timer -= timerTick;
            }
        }
        else
        {
            RaycastHit2D walkRight = Physics2D.Raycast(transform.position, new Vector2(.5f, -1f), .5f, PlatformLayerMask);
            RaycastHit2D walkLeft = Physics2D.Raycast(transform.position, new Vector2(-.5f, -1f), .5f, PlatformLayerMask);

            //Check for platform end
            if (prevState != CurrentState.Idle && ((walkLeft.collider != null && walkRight.collider == null) || (walkRight.collider != null && walkLeft.collider == null)))
            {
                prevState = currentState;
                animator.SetBool("Moving", false);

            }
            else
            {
                if (walkRight.collider != null && prevState != CurrentState.MovingLeft)
                {
                    currentState = CurrentState.MovingRight;
                    animator.SetBool("Moving", true);
                    moveDirection = 1;

                }

                if (walkLeft.collider != null && prevState != CurrentState.MovingRight)
                {

                    currentState = CurrentState.MovingLeft;
                    animator.SetBool("Moving", true);
                    moveDirection = -1;
                }

                prevState = currentState;
                transform.position += Vector3.right * Time.deltaTime * speed * moveDirection;

                transform.localScale = new Vector3(-moveDirection, 1, 1);

            }


        }
    }

    protected override void Death()
    {
        gc.Score += scoreValue;
        Destroy(gameObject);
    }

    public override void Damaged(int damage)
    {
        hp -= damage;
        if (hp <= 0)
        {
            Death();
        }
    }

    private void CheckAttackDistance()
    {
        if (currentState == CurrentState.MovingRight)
        {
            RaycastHit2D attackR = Physics2D.Raycast(transform.position, Vector2.right, .5f, PlayerLayerMask);
            if (attackR.collider != null)
            {
                Debug.Log("Player!!");
            }

            Debug.DrawRay(transform.position, Vector3.right * .5f, Color.red);
        }

        else if (currentState == CurrentState.MovingLeft)
        {
            RaycastHit2D attackL = Physics2D.Raycast(transform.position, Vector2.left, .5f, PlayerLayerMask);


            Debug.DrawRay(transform.position, Vector3.left * .5f, Color.red);
            if (attackL.collider != null)
            {
                Debug.Log("Player!!");
            }

        }

    }
}