David Liu

Child Arcana

About the Game

Child Arcana is 2D platformer remake of the classic game: Kid Icarus. Ascend from the underworld and fight your way through several ravenous monsters. Shooting enemies with your bow will get you points or health, along with point pickups in the level as you rise up to the overworld. The game only remakes the first level of Kid Icarus, but we did our best to recreate as much of the level as we could.

Developed in Retro Remake Jam 2017

Play the game here!

Role in Development

  • Manager: Hector Piñeiro II
  • Designer: Hector Piñeiro II,
  • Programmers: Hector Piñeiro II, David Liu
  • Artist: Leaf Mautrec

This was the very first game jam I took a part of. My friend, Hector, was very kind in assisting me through the game jam. He taught me about basic platformer mechanics, basic AI movement, and lots of Unity tricks and practices. Since I still wasn't knowledgeable with much programming for games nor with using Unity as an engine at the time, I helped contribute to simple scripts such as player movement and attributes.

Script Examples

PlayerMovement.csStats.csInvincibility.csSpecknoseAI.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    public float jumpHeight = 3 * 1.28f;
    public float totalJumpTime = 1.0f;
    public float walkSpeed = 3 * 1.28f;

    public bool isCrouch = false;
    public bool isUp = false;
    public bool isLeft = false;
    public Vector2 displacement;

    private Rigidbody2D rigidBody;
    private BoxCollider2D collider;

    private bool canJump = false;
    private bool isGrounded = false;
    private bool isTouchingWall = false;
    private int layerIndex;
    private float jumpTimer = 0;
    private HashSet collidedGroundObjects;
    private AudioManager audioManager;
    private RaycastHit2D hit;

    private Vector3 startPosition;

    void Awake()
    {
        startPosition = transform.position;
    }

    // Use this for initialization
    void Start () {
        rigidBody = gameObject.GetComponent();
        collider = gameObject.GetComponent();
        collidedGroundObjects = new HashSet();
        audioManager = GameObject.Find("Audio").GetComponent();
        layerIndex = 1 << LayerMask.NameToLayer("Block");

        // Reset player on Start so it doesn't fly through walls at the beginning of the scene
        displacement = Vector3.zero;
        rigidBody.velocity *= 0;
        transform.position = startPosition;
    }

    void OnCollisionEnter2D(Collision2D c)
    {
        OnCollisionStay2D(c);
    }

    void OnCollisionStay2D(Collision2D c)
    {
        if (c.contacts.Length > 0)
        {
            ContactPoint2D contact = c.contacts[0];

            // If the collision was below the player, the player is grounded
            if (rigidBody.velocity.y <= 0 && displacement.y <= 0 && Vector3.Dot(contact.normal, Vector2.up) > 0.75f)
            {
                ResetJump();
                isGrounded = true;
                collidedGroundObjects.Add(c.gameObject);
            }

            // If player hits its head, end the jump
            else if (Vector3.Dot(contact.normal, Vector2.up) < -0.75f)
            {
                if (canJump)
                {
                    audioManager.PlayDenySound();
                }

                jumpTimer = totalJumpTime;
                canJump = false;
            }
        }
    }

    void OnCollisionExit2D(Collision2D c)
    {
        collidedGroundObjects.Remove(c.gameObject);

        // If there are no game objects directly under the player, it's no longer grounded
        if (collidedGroundObjects.Count == 0)
        {
            isGrounded = false;
        }
    }

    // Update is called once per frame
    void Update()
    {
        float verticalInput = Input.GetAxis("Vertical");

        // Pit crouches
        if (verticalInput < 0)
        {
            isCrouch = true;
            isUp = false;
            collider.size = new Vector2(1, 1.92f * (2f / 3f));
            collider.offset = new Vector2(0, 0.95f * (2f / 3f));
        }
        else if (verticalInput > 0) // Pit aims upward
        {
            isCrouch = false;
            isUp = true;
            collider.size = new Vector2(1, 1.92f);
            collider.offset = new Vector2(0, 0.95f);
        }
        else // Pit is standing/jumping/falling
        {
            isCrouch = false;
            isUp = false;
            collider.size = new Vector2(1, 1.92f);
            collider.offset = new Vector2(0, 0.95f);
        }

        // Reset movement for this tick
        displacement = Vector2.zero;

        // Shoots the raycast in the proper direction
        if (isLeft)
        {
            hit = Physics2D.Raycast(transform.position, Vector3.left, 0.64f, layerIndex);
        }
        else
        {
            hit = Physics2D.Raycast(transform.position, Vector3.right, 0.64f, layerIndex);
        }

        // Hits a block and stops the player
        if (hit.collider != null && hit.collider.tag == "Block")
        {
            // Makes sure the player can still collide with the door to finish the game
            if(hit.collider.name == "FinishDoor")
            {
                isTouchingWall = false;
            }
            else
            {
                isTouchingWall = true;
            }
        }
        else
        {
            isTouchingWall = false;
        }

        UpdateMovement();
        UpdateJump();

        rigidBody.position += new Vector2(displacement.x, displacement.y);
    }

    private void UpdateMovement()
    {
        float walkDirection = Input.GetAxis("Horizontal");

        // Determines the direction the player is facing
        if(walkDirection < 0)
        {
            isLeft = true;
        }
        else if(walkDirection > 0)
        {
            isLeft = false;
        }

        // Add movement from walking around
        if(!((isUp || isCrouch) && isGrounded) && !isTouchingWall) // Prevents Pit from moving along the ground while aiming up or crouching or touching a wall
        {
            displacement.x += walkDirection * walkSpeed * Time.deltaTime;
        }
    }

    private void UpdateJump()
    {
        bool jumpKeyDown = Input.GetButtonDown("Jump") || (jumpTimer > 0 && Input.GetButton("Jump"));
        float prevJumpTimer = jumpTimer;
        float deltaTimeSinceLastJump = Time.deltaTime;

        // If no jump key is down, or the jump has finished, the player can no longer jump
        if (!jumpKeyDown || jumpTimer >= totalJumpTime)
        {
            canJump = false;
        }

        // Otherwise, if the player can jump, jump!
        else if (canJump)
        {
            // Play the jump sound if this is the beginning of the jump
            if (jumpTimer == 0)
            {
                audioManager.PlayJumpSound();
            }

            if (jumpTimer < totalJumpTime)
            {
                jumpTimer += deltaTimeSinceLastJump;

                // Do not allow the jump timer to exceed the total time allow for holding a jump
                if (jumpTimer > totalJumpTime)
                {
                    deltaTimeSinceLastJump = jumpTimer - totalJumpTime;
                    jumpTimer = totalJumpTime;
                }
            }
        }

        // If the player is jumping, prepare to move this game object up in space
        if (canJump && jumpTimer > 0)
        {
            displacement += Vector2.up * CalculateJumpIncrement(prevJumpTimer, jumpTimer);

            // Undo the gravity that was applied since the jump calculation will already be parabolic
            Vector2 currentFrameGravity = Physics2D.gravity * (jumpTimer - prevJumpTimer) * rigidBody.mass;
            rigidBody.AddForce(-currentFrameGravity, ForceMode2D.Impulse);
        }

        // Reset jump variables if grounded and can jump
        if (isGrounded && !canJump)
        {
            ResetJump();
        }
    }

    /**
     * Returns the distance the player should rise from t0 seconds to t1 seconds in the jump cycle to abide by a parabolic motion
     */
    private float CalculateJumpIncrement(float t0, float t1)
    {
        float offset0 = t0 - totalJumpTime;
        float offset1 = t1 - totalJumpTime;
        return (jumpHeight / (totalJumpTime * totalJumpTime * totalJumpTime)) * (offset1 * offset1 * offset1 - offset0 * offset0 * offset0);
    }

    private void ResetJump()
    {
        jumpTimer = 0;
        canJump = true;
    }
}