Player

The Player avatar in the scene is the cat.
It has a fully animated geometry I got from here: Trash Dash
The player moves by means of Unity’s built-in CharacterController.

Player.cs

Let’s take a look at the Player script.

public class Player : MonoBehaviour
{
    public float m_trackSpacing = 3.0f;
    public Swiper m_swiper;
    public float m_fwdSpeed = 10.0f;
    public float m_sideSpeed = 10.0f;
    public float m_jumpSpeed = 10.0f;
    public float m_gravity = 10.0f;
    public float m_maxDownSpeed = 100.0f;
    public float m_crashSpeed = 5.0f;
    public float m_crashDrag = 20.0f;
    public GameObject m_pauseMenu;
    public GameObject m_gameOver;

    int m_zone = 1;
    float m_ySpeed = 0.0f;
    float m_crashZSpeed = 0.0f;
    CharacterController m_charController;
    bool m_crashed = false;
    bool m_isPaused = false;
    Animator m_anim;

    void Start()
    {
        if (null == m_swiper)
        {
            m_swiper = FindFirstObjectByType<Swiper>();
        }
        m_swiper.SwipeLeftEvent += SwipeLeft;
        m_swiper.SwipeRightEvent += SwipeRight;
        m_swiper.SwipeUpEvent += SwipeUp;
        m_swiper.SwipeDownEvent += SwipeDown;
        m_charController = GetComponent<CharacterController>();
        m_anim = GetComponentInChildren<Animator>();
        SetPause(false);
        m_gameOver.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        Vector3 move = Vector3.zero;
        Vector3 pos = transform.position;
        float targetX = (m_zone - 1) * m_trackSpacing;
        float delta = targetX - pos.x;
        if (Mathf.Abs(delta) <= m_sideSpeed * Time.deltaTime)
        {
            move.x += delta;
        }
        else
        {
            move.x += Mathf.Sign(delta) * m_sideSpeed;
        }
        if (m_charController.isGrounded)
        {
            m_ySpeed = Mathf.Max(m_ySpeed, -m_gravity);
        }
        else
        {
            m_ySpeed -= m_gravity * Time.deltaTime;
            m_ySpeed = Mathf.Max(m_ySpeed, -m_maxDownSpeed);
        }
        move.y += m_ySpeed;
        if (m_crashed)
        {
            m_crashZSpeed -= m_crashDrag * Time.deltaTime;
            m_crashZSpeed = Mathf.Max(m_crashZSpeed, 0.0f);
            move.z -= m_crashZSpeed;
        }
        else
        {
            move.z += m_fwdSpeed;
        }
        m_charController.Move(move * Time.deltaTime);

        // keys
        if (Input.GetKeyDown(KeyCode.Escape))
        {   // this doubles as the option key in the android navigation bar
            SetPause(!m_isPaused);
        }
    }

    void ChangeTrack(int newTrack)
    {
        m_zone = newTrack;
    }

    void SwipeLeft()
    {
        SwipeText("Left");
        if (false == m_crashed && false == m_isPaused)
        {
            if (m_zone > 0)
            {
                ChangeTrack(m_zone - 1);
            }
        }
    }

    void SwipeRight()
    {
        SwipeText("Right");
        if (false == m_crashed && false == m_isPaused)
        {
            if (m_zone < 2)
            {
                ChangeTrack(m_zone + 1);
            }
        }
    }

    void SwipeUp()
    {
        SwipeText("Up");
        if (false == m_crashed && false == m_isPaused)
        {
            if (m_charController.isGrounded)
            {
                m_ySpeed = m_jumpSpeed;
                if (null != m_anim)
                {
                    m_anim.SetTrigger("Jump");
                }
            }
        }
    }

    void SwipeDown()
    {
        SwipeText("Down");
        if (false == m_crashed && false == m_isPaused)
        {
            if (false == m_charController.isGrounded)
            {
                m_ySpeed = -m_jumpSpeed;
            }
            if (null != m_anim)
            {
                m_anim.SetTrigger("Duck");
                StartCoroutine(DuckSlide());
            }
        }
    }

    private void SwipeText(string text)
    {
        MessageBox.Message(text);
    }

    private void OnControllerColliderHit(ControllerColliderHit hit)
    {
        if (false == m_crashed)
        {
            if (hit.normal.y < 0.8f)
            {
                Debug.Log("Hit Collision " + hit.gameObject.name);
                m_crashed = true;
                m_crashZSpeed = m_crashSpeed;
                if (null != m_anim)
                {
                    m_anim.SetTrigger("Crash");
                }
                StartCoroutine(GameOver());
            }
        }
    }

    IEnumerator DuckSlide()
    {
        yield return new WaitForSeconds(0.5f);
        if (null != m_anim)
        {
            m_anim.ResetTrigger("Duck");
        }
    }

    IEnumerator GameOver()
    {
        m_gameOver.SetActive(true);
        // wait 3 seconds
        yield return new WaitForSecondsRealtime(3.0f);
        // and reload the scene
        SceneManager.LoadScene(0);
    }

    public void SetPause(bool setPause)
    {
        if (setPause)
        {
            Time.timeScale = 0.0f;
        }
        else
        {
            Time.timeScale = 1.0f;
        }
        m_pauseMenu.SetActive(setPause);
        m_isPaused = setPause;
    }

    public void QuitGame()
    {
        Application.Quit();
    }
}

In Update() you’ll see that the movement is passed into m_charController.
The track is divided into 3 “zones”: left, right, and center.
The side-to-side motion is controlled by m_zone… the character is pushed towards the location of m_zone.
Functions SwipeLeft() and SwipeRight() change the player’s target m_zone through calls to ChangeTrack().
But what calls SwipeLeft() and SwipeRight()?
Up in the Start() function, you’ll see there is a Swiper, and these functions are registered with that.

Swiper.cs

I won’t paste the whole thing… you can go look at it.
You should see that it implements the UI drag handlers and the real work happens in CheckDrag()
This must mean there is a Swiper component somewhere in the Scene’s UI.
You will find it as “Canvas/SwipeZone” in the Scene hierarchy, and you’ll see that it fills the entire screen.

FollowCam.cs

As the player runs, we need the camera to keep up.
In the Scene, “MainCamera” has a FollowCam on it.
If you look at the script, you’ll see it uses InverseTransformPoint() and TransformPoint() to keep the camera’s position relative to the player.
Also of note is the use of LateUpdate() to ensure that the player is updated before the camera.

TrackController.cs

Finally, we need to keep making more track as you run.
TrackController.Update() generates new track in sections as you go, and deletes them as they fall behind.
We have an array of m_trackPrefabs to randomly select from to keep the game varied.

Object Pools

ObjectPool.cs

This file has a lot of TODOs in it. This is where a lot of our work will take place.
An ObjectPool is effectively a List<> of GameObjects available for use.
When you need to spawn a particular type of object (in our case a banana), you pull one from the matching ObjectPool.
When you’re done with that object, you won’t delete it. Return it to the ObjectPool instead.
Each ObjectPool has only 1 type of object in it. (We’ll have 2: bananas, and pickup effects.)

PooledObject.cs

When you want to make an ObjectPool out of something, use PooledObject as a base class to keep track of which ObjectPool it came from.

SpawnPools.cs

We have 2 ObjectPools in our game: bananas and pickup effects.
To access the pools from anywhere, I have set up a class SpawnPools with public variables to access the pools.
SpawnPools has a public getter SpawnPools.Get() to make it accessible from anywhere.
In a real version of this game, we might want pools for lots of other stuff as well, but this should be enough to get the idea across.

Trash Dash is a complete Endless Runner tutorial.
Unity’s tutorials are a great way to take things beyond the scope of this class.