One By One Design

Unity3D Endless Runner Part I – Curved Worlds

A couple months back I got an overpowering hankerin’ to create an endless runner style game in Unity3D (think Subway Surfers or Temple Run). Now, it’s been quite some time since I’d even opened Unity3D, let alone made anything so, after updating my free version install to 5.3.5f1, I figured the best thing to do would be to look around for a decent tutorial. I definitely found a couple, but I didn’t actually like the looks of them, so I decided to just spend a couple weekends creating my own instead. Since I did, I thought I’d post my results here to possibly help anyone else with a similar wild hair.

Seeing as I’m by no means a Unity expert and am relatively new to C#, I wouldn’t consider this a tutorial, but rather a description of a journey. If anyone who actually knows Unity reads this and wonders why I did something in some very odd fashion, there’s a 99.9% chance it’s because I didn’t know any better. Post a comment if you have any best practices tips.

First things first, since I rejected several tutorials after looking at the final product, it’s only fair to show what I set out to make. You can check out the final result here (your mileage may vary – use arrow keys to slide left and right – there is no jump or duck). It’s published to the Unity web player, so you’ll need to view in a browser that supports the plugin (Firefox, e.g). If it’s something you have no interest in, you can quit reading here and find something better to do. Otherwise…

The first thing I wanted to do for the ‘game’ I wanted to make was figure out how to ‘bend’ the horizon. This is actually a pretty ass backwards way to go since this is more polish rather than core game mechanics. The thing I disliked about all the tutorials I checked out, though, was that the adding of terrain in the distance was always obvious and, well, ugly. I wanted the terrain be added past the curve of the horizon so it wouldn’t simply and suddenly appear. If I couldn’t figure this part out, there was no point in continuing the project.

A bit of googling let me know the expression for what I wanted was “horizon bending” or “world bending” or “world curving” etc (there seems to be no real consensus, actually). It didn’t take long to find this fantastic Unity vertex shader by Davit Naskidashvili on the Asset Store, Curved World. Now that seemed exactly what I was looking for, and I’ll probably get it someday, but 45 bucks for a throwaway learning experience game seemed a bit excessive. A bit more googling found a couple vertex shader examples here and here. Again, I’m no CG shader expert (actually better with AGAL of all things), but it seemed simple enough to combine those two examples to get what I wanted.

I won’t go into great detail about the shader I came up with as it’s fairly self-explanatory. There’s a bend amount in the x and y planes that (rather hackily) gets reduced to a fraction of itself. Each model vertex is converted to world space. The distance between the vert and the origin of the bend is calculated, maxed, squared and multiplied by the reduced amount of bend. That then gets added to vert’s x and y which is then converted back to object space and applied to the model. You can check out the full shader below:

//
// Curved.shader
//
// Author:
//       Devon O. <devon.o@onebyonedesign.com>
//
// Copyright (c) 2016 Devon O. Wolfgang
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.


Shader "Custom/Curved"
{
    Properties
    {
        _Color ("Main Color", Color) = (1,1,1,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Lambert vertex:vert addshadow

        // Global Shader values
        uniform float2 _BendAmount;
        uniform float3 _BendOrigin;
        uniform float _BendFalloff;

        sampler2D _MainTex;
        fixed4 _Color;

        struct Input
        {
              float2 uv_MainTex;
        };

        float4 Curve(float4 v)
        {
              //HACK: Considerably reduce amount of Bend
              _BendAmount *= .0001;

              float4 world = mul(_Object2World, v);

              float dist = length(world.xz-_BendOrigin.xz);

              dist = max(0, dist-_BendFalloff);

              // Distance squared
              dist = dist*dist;

              world.xy += dist*_BendAmount;
              return mul(_World2Object, world);
        }

        void vert(inout appdata_full v)
        {
              v.vertex = Curve(v.vertex);
        }

        void surf(Input IN, inout SurfaceOutput o)
        {
              fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
              o.Albedo = c.rgb;
              o.Alpha = c.a;
        }

        ENDCG
    }
 
      Fallback "Mobile/Diffuse"
}

One thing to note about the shader is that the properties responsible for the curve (_BendAmount, _BendOrigin, and _BendFalloff) are not included in the ‘properties’ block. This is so they can be set globally through static methods of Unity’s Shader class.

Now let’s see how to put this shader to use. Open up Unity and create a new project. Once there, import the necessary assets. For this project I used some absolutely fantastic free assets from the Unity asset store. For the player character, I used Chibi Mummy created by Ricochet. And for the terrain, I used Grass Road Race from Sugar Asset. Actually, for this first part of the ‘tutorial’, the Grass Road Race are all the assets needed.

In your Assets directory, create a new folder named ‘Shaders’ and add the above shader in a file named ‘curved.shader’. Now, the Grass Road Race assets package comes with prefabs, but I wanted to create my own from them, so also in your Assets directory create a new folder named ‘Prefabs’. From the GRR/Prefab folder, drag the prefab named Ground01 into your Hierarchy panel to add it to the scene and rename it ‘Tile01’. In the Inspector panel, set the transform position to (0, 0, 0). Twirl down the Tile01 object in Hierarchy panel and you’ll see there are two models inside: ‘Fence’ and ‘Ground’. Select each of these and, in the Inspector panel texture section (Tex_Fence or Tex_Ground), use the dropdown box to change the shader to ‘Custom/Curved’. Now, drag that Tile01 object into the ‘Prefabs’ folder you created to create a prefab then delete it from the scene. Back in the GRR/Prefab folder, drag out the prefab named ‘Bridge_Large’, name it ‘Tile02’ and go through the same process.

So, now you should have 2 prefabs in your Prefabs directory named Tile01 and Tile02 and nothing but a main camera and directional light in the scene. Just because I tested this and know it looks pretty good, set the position of your camera to (0, 1.25, -7). Drag a Tile01 instance into the Hierarchy panel to add it to the scene at (0, 0, 0). Add another Tile01 and 2 Tile02 instances (so you now have 4 tiles in the scene). Set the z position of the next tile to 7.62 (a magic number we’ll see again in a later post, assuming I write it up). The next tile to z 15.24 (7.62*2) and the last to z 22.86 (7.62*3) that should get the tiles all lined up in a nice row. Looking at the Game Panel, you should see something similar to this:

Four Tiles

Back in the Hierarchy Panel create an empty GameObject and name it ‘CurveController’. In the Shaders directory we created earlier, create a new C# script also named ‘CurveController.cs’ (I don’t know if it’s a good practice to give scripts the same name as GameObjects, but what the hell. If anyone has thoughts about that, let me know). Again, I won’t go into great detail about the script because it’s fairly self explanatory. Essentially it gives some slider controls to adjust those Bend properties used by the shader posted above. It also does so within the editor, so there’s no need to hit play to test. The CurveController script is below:

//
// CurveController.cs
//
// Author:
//       Devon O. <devon.o@onebyonedesign.com>
//
// Copyright (c) 2016 Devon O. Wolfgang
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.


using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
public class CurveController : MonoBehaviour
{

    public Transform CurveOrigin;

    [Range(-500f, 500f)]
    [SerializeField]
    float x = 0f;

	[Range(-500f, 500f)]
    [SerializeField]
    float y = 0f;

	[Range(0f, 50f)]
	[SerializeField]
	float falloff = 0f;

    private Vector2 bendAmount = Vector2.zero;

    // Global shader property ids
    private int bendAmountId;
    private int bendOriginId;
    private int bendFalloffId;

    void Start ()
    {
        bendAmountId = Shader.PropertyToID("_BendAmount");
        bendOriginId = Shader.PropertyToID("_BendOrigin");
        bendFalloffId = Shader.PropertyToID("_BendFalloff");
	}
	
	void Update ()
    {
        bendAmount.x=x;
        bendAmount.y=y;

        Shader.SetGlobalVector(bendAmountId, bendAmount);
        Shader.SetGlobalVector(bendOriginId, CurveOrigin.position);
        Shader.SetGlobalFloat(bendFalloffId, falloff);
	}
		
}

Drag that script onto the CurveController GameObject in the Hierarchy window. When you select that GameObject, you should be able to see the exposed controls. You’ll also see an empty ‘Curve Origin’ box which expects a Transform instance. Just drag your main camera to that box, making sure it appears there afterwards.

Now the moment of truth. If all went well, you should be able to drag the sliders of that CurveController script and see your row of Tile objects bend and twist. The X control will make the road turn left and right. The Y control will make the road curve up and down (like a hill or a scene from Inception). The Falloff control will create a ‘flat’ area extending from the CurveOrigin (i.e. the camera in this example) before applying the curve.

And, because the shader creates the curve from the Curve Origin, it should update if you move the camera along the z axis like so:

Camera Movement

And that’s it for Part the First. We now have a nice curved road we can run along endlessly. The next part will actually endlessly animate that road and add a player that can scoot back and forth. After that, we’ll just need some obstacles to dodge.

Posted by

Post a comment

Your email address will not be published. Required fields are marked *