Mao Yidi
Dark underground laboratory with a glowing time machine, from the game Qiyu.

Qiyu

Unity Game 3

A third-person Unity game telling the survival crisis of penguins.

Qiyu - full gameplay walkthroughPlay on this page (loads YouTube)

Chapter 1: Main Character

Protagonist

The Researcher

The protagonist is a young researcher who works in a time travel lab. He is responsible for traveling back in time to research the living conditions of penguins in the past, and to find out the reason why penguins are facing extinction in the future.

The researcher in a red suit walking across a green low-poly meadow The researcher seen from behind on a snowy shore with mountains ahead Concept drawing of the researcher in a red jumpsuit with goggles and a scanner

Gears

The scanner on his right waist can capture detailed stats of penguins, and the data will be sent back to lab for analysis. His specialized glasses also have scanning function, and can identify encountered penguin types.
His all-in-one suit can protect him from all extreme climates, making him survive the icy water of polar region and immune to any fall damage.

Close-up of the researcher's belt, pouches and scanner on his right waist The researcher in his red suit standing in a pool of light against a dark background

Penguins' Soul

The mentor

Penguins' Soul is the only NPC in the game, who taught the protagonist a lot about the past of penguins, and the reason why they are facing extinction in the future. Also, throughout the adventure, the protagonist will receive numerous important hints from Penguins' Soul.

Penguins' Soul, a blue-caped bird in a witch hat, hovering near the researcher on the ice Close-up of Penguins' Soul with its blue cape and pointed hat against a pale sky

Chapter 2: Level Design

Time Travel Lab

Time Setting: 3022

This is an underground time travel lab, also the entrance level of the game, where players get to know about the background story of the game. After we clearly inform the players about their duties, they step into the machine onto a trip to 200,000 years ago.

The time machine glowing cyan in a dark laboratory with pipes and equipment Racks of server equipment with green indicator lights in the lab

Zealandia

Time Setting: 200,000 years ago

This is a much brighter larger outdoor scene. The player can freely explore the world and research for the habitat patterns of the earliest species of penguins.
Also, upon entering the world, the player will get to meet the first NPC of the game -- Penguins' Soul, who will guide the player through the game.

Penguins' Soul floating over a winding path in a low-poly green valley with mountains The researcher seen from behind overlooking a hilly low-poly landscape A low-poly penguin standing beside a tree and red flowers in the valley

Antarctica

Time Setting: 2122

This level was to portray how the polar region would look like a hundred years later. There are few floating ice on the ocean face as ice are melting due to extreme climates. As a result, it can be clearly seen throughout the scene that: penguins' living space is exploited and their living condition is worsening.

Two penguins on a small ice floe with distant mountains Penguins' Soul flying over open water and a lone ice floe with the researcher on it Aerial view of a single ice floe surrounded by blue ocean

Camera Zoom

Zooms the Third-Person-Cam based on player's sprinting status or mouse scroll input. It also helps focus on essential elements in the scene.

using UnityEngine;
using DG.Tweening;

public class CameraZoom : MonoBehaviour
{
    [Header("FOV")] 
    public float fovRange = 8f;
    public float fovDelta = 4f;

    [Header("Camera Distance")] 
    public float distanceRange = 0.25f;
    public float distanceDelta = 0.2f;

    [Header("Factors")] 
    public float zoomDuration = 0.3f;
    public float zoomInFactor = 1.5f;
    public float rightOffsetFactor = 0.3f;
    public float scrollFactor = 0.025f;

    private vThirdPersonCamera cam;
    private float FOV, FOVmin, FOVmax, FOVdefault, tempFOV, tempDistance, defaultDistance, defaultRightOffset;

    private void Start()
    {
        cam = GetComponent<vThirdPersonCamera>();
        defaultDistance = cam.defaultDistance;
        tempDistance = defaultDistance;
        defaultRightOffset = cam.rightOffset;

        FOV = GetComponent<Camera>().fieldOfView;
        FOVdefault = FOV;
        tempFOV = FOV;
        SetMinMax();
    }

    private void Update()
    {
        if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.D))
            if (Input.GetKeyDown(KeyCode.LeftShift))
                ZoomOut();

        if (Input.GetKeyUp(KeyCode.LeftShift))
            ZoomToDefault();

        if (Input.GetMouseButtonDown(1))
            ZoomIn();

        if (Input.GetMouseButtonUp(1))
            ZoomToDefault();

        if (Input.mouseScrollDelta.y != 0)
            ZoomOnMouseScroll();
    }

    private void ZoomIn()
    {
        tempFOV = FOV;
        FOVmin -= fovDelta * 2f;

        DOTween.To(() => FOV, x => FOV = x, FOV - fovDelta * 2f, zoomDuration * zoomInFactor)
            .OnUpdate(() => GetComponent<Camera>().fieldOfView = FOV);

        DOTween.To(() => tempDistance, x => tempDistance = x, tempDistance - distanceDelta * 1.75f, zoomDuration * zoomInFactor)
            .OnUpdate(() => cam.defaultDistance = tempDistance);

        DOTween.To(() => defaultRightOffset, x => defaultRightOffset = x, defaultRightOffset + distanceDelta * rightOffsetFactor, zoomDuration * zoomInFactor)
            .OnUpdate(() => cam.rightOffset = defaultRightOffset);
    }

    private void ZoomOut()
    {
        tempFOV = FOV;
        FOVmax += fovDelta;

        DOTween.To(() => FOV, x => FOV = x, FOV + fovDelta, zoomDuration)
            .OnUpdate(() => GetComponent<Camera>().fieldOfView = FOV);

        DOTween.To(() => tempDistance, x => tempDistance = x, tempDistance - distanceDelta, zoomDuration)
            .OnUpdate(() => cam.defaultDistance = tempDistance);
    }

    private void ZoomToDefault()
    {
        DOTween.To(() => FOV, x => FOV = x, tempFOV, zoomDuration)
            .OnUpdate(() => GetComponent<Camera>().fieldOfView = FOV);

        DOTween.To(() => cam.defaultDistance, x => cam.defaultDistance = x, tempDistance, zoomDuration)
            .OnUpdate(() => cam.defaultDistance = tempDistance);

        DOTween.To(() => cam.rightOffset, x => cam.rightOffset = x, defaultRightOffset, zoomDuration).OnUpdate(() => cam.rightOffset = defaultRightOffset);
        SetMinMax();
    }

    private void ZoomOnMouseScroll()
    {
        float scrollDelta = Input.mouseScrollDelta.y;

        tempDistance -= scrollDelta * scrollFactor;
        tempDistance = Mathf.Clamp(tempDistance, defaultDistance - distanceRange, defaultDistance + distanceRange);
        cam.defaultDistance = tempDistance;

        FOV -= scrollDelta * scrollFactor * 20;
        FOV = Mathf.Clamp(FOV, FOVmin, FOVmax);
        GetComponent<Camera>().fieldOfView = FOV;
    }

    private void SetMinMax()
    {
        FOVmax = FOVdefault + fovRange;
        FOVmin = FOVdefault - fovRange * 0.75f;
    }
}

Typewriter Effect

  1. Takes user input as a string array.
  2. Advances through the array index on internal timing or mouse click.
  3. Breaks down the input string into characters.
  4. Uses IEnumerator for gradual character display with minor interval.
using UnityEngine;
using System.Collections;
using System;
using TMPro;

public class DialogPlayer : MonoBehaviour
{
    public Dialog[] dialogs;
    public TextMeshProUGUI dialogText, nameText;
    public float characterInterval = 0.05f, autoNextLineTime = 2.5f;
    private float _elapsedTime;
    private int _index;

    private void Start()
    {
        dialogText.text = string.Empty;
        nameText.text = string.Empty;
        StartCoroutine(TypeLine());
    }

    private void Update()
    {
        _elapsedTime += Time.deltaTime;
        if (Input.GetMouseButtonDown(0) || Input.GetKeyDown(KeyCode.Return) || _elapsedTime > autoNextLineTime) SwitchLine();
    }

    private void SwitchLine()
    {
         _elapsedTime = 0;
        if (dialogText.text == dialogs[_index].dialog)
        {
            NextLine();
        }
        else
        {
            StopAllCoroutines();
            dialogText.text = dialogs[_index].dialog;
        }
    }

    private void NextLine()
    {
        dialogText.text = string.Empty;
        if (_index < dialogs.Length - 1)
        {
            _index++;
            StartCoroutine(TypeLine());
            nameText.text = dialogs[_index].name;
        }
        else
        {
            gameObject.SetActive(false);
        }
    }

    private IEnumerator TypeLine()
    {
        foreach (char x in dialogs[_index].dialog)
        {
            dialogText.text += x;
            yield return new WaitForSeconds(characterInterval);
        }
    }

    [Serializable]
    public struct Dialog
    {
        public string name;
        public string dialog;
    }
}