Team Racing League

Team Racing League is a team-based, top-down multiplayer party game (quite a mouthful!). In the main game mode, two teams of three players take on each other. Only one team member needs to finish all the laps, which allows other team members to choose to obstruct the other team. 

During my internship at Gamious (February-July 2016), and later employment, I was responsible for executing the art direction, by designing and producing the game’s race tracks and optimizing production. The biggest challenge was to create demarcations in the most natural way possible, so that the tracks don’t look man-made. Since it’s a team-based game, it was imperative that players would be able to tell what’s going on in the game in a glimpse. Therefore the environments had to be like a blank canvas, not too distracting. So details had to be limited, but still present.

Four different settings were in mind: Desert. mountain, forest and snow.

Mountain track, from block out to final

Environment design

The forest and snow setting did not have a clear design, other than them being an idea in mind. So the production of the tracks in these teams were prefaced by art direction tests. Where communication being me, the producer and creative director steered it in the right direction. Eventually determining the theme’s unique properties.

Forest track, initial art progress

For the snow theme I started by taking an existing track and using it as a playground to test the required shaders I needed. The main snow shader “covers” everything with snow from the Y-axis and adds snow border on the bottom of all objects. Due to the nature of some objects, I found it was useful to create a texture atlas for them, since this theme holds a lot of objects that are resused and bashed together. Unlike the other themes which have unique cliffs and walls.

Snow theme, art direction concepts

The texture atlas was created in Substance Designer. So that all separate material could be edited without quality loss.

Lastly I starting making the building blocks for the environments. Several types of rock, earth borders and modulair spline sections.

Shaders/Materials

The artist, prior to my arrival had intended to use a texture atlas for each setting. However, down the line and during iteration that would require an immense amount of time spent on UV maps. Given the small amount of materials in one environment, I opted to create a set of world-space tiled tri-planar shaders. Using such a shader would remove the need to create and re-adjust the UV maps, and would ensure seamless tiling between different objects. Due to static batching the performance cost of the shader was negligible.

The textures used in the shaders are sourced from a Substance material, which functions as a sort of library of noise and texture overlays, each with a set of controls. This allowed me and the lead-artist to iterate on the look of the theme in real-time.

I also introduced Substance to be used for the player/car model. Since there were several car colors and every color had 4 different variations, so team members could tell each other apart. It seemed unnecessary to have 16 bitmap files when a single Substance with a color parameter exposed could achieve the same.

Workflow tools

The environments were all build in 3DSMax, around a plane that indicated the camera frustum, and exported as an FBX file. This model would be placed in a scene and materials, colliders and scripts would be assigned. Since most of the game’s systems were built before Unity’s additive scene loading feature it was not possible to load in these environment scenes. Instead prefabs were used and loaded/unloaded when necessary.

The primary issue that occurred with this was creating a prefab from the environment would break its “prefab” connection to the FBX file. When that happened any updates or changes made to the FBX file would not be reflected in Unity. I solved this by creating an editor script that does the following:

  • Add ObjectLightmapData component to all active MeshRenderers
  • Duplicate track (level) model prefab object
  • Remove ObjectLightmapData components from original object
  • Parent the Data object (holding all effects, waypoints and other info) to the duplicate
  • Create a prefab from the duplicate
  • Render a screenshot and save it as the new track thumbnail

The second issue was that the lighting baked in the environment scene and graphical settings (scene settings and camera post-effects) would could not be carried over. Loading in the correct lightmaps in the “main” scene wouldn’t do anything, since creating a prefab removes all object’s lightmap information normally stored in the Renderer component (lightmap index, scale and offset. Normally stored in the LightingAsset file). And the settings of all the camera post effects were stored in the environment scene’s camera.

A workaround for the lightmaps was to store this information on all meshes through an attached script, in the scene where the lighting information was baked. When track prefab is instantiated the correct lightmap information is restored to the renderer components.

[accordion] [pane title=”TrackCreator.cs” start=closed]
private void CreatePrefab()
    {
        selectedGO = Selection.activeGameObject;

        trackname = selectedGO.name;
        trackpath = prefabPath(trackname);

        //Add LightMapData script to each static meshrenderer
        lmdb.addComponents();

        //Duplicate track model object
        GameObject trackPrefab = (GameObject)Instantiate(selectedGO, selectedGO.transform.position, selectedGO.transform.rotation) as GameObject;
        trackPrefab.name = trackname;

        //New object has LightMapData, remove it from original object so the action of adding it can be repeated safely.
        lmdb.removeComponents();

        //Duplicate data object to preserve any possible prefabs used
        GameObject trackData = (GameObject)Instantiate(GameObject.Find(trackname + "Data"), Vector3.zero, Quaternion.identity) as GameObject;
        trackData.name = trackname + "Data";

        //Parent it to the new track prefab object
        trackData.transform.parent = trackPrefab.transform;

        //Create track prefab from new object
        PrefabUtility.CreatePrefab(prefabPath(trackname) + trackname + ".prefab", trackPrefab, ReplacePrefabOptions.ReplaceNameBased);

        GameObject newPrefab = (GameObject)AssetDatabase.LoadAssetAtPath(trackpath + trackname + ".prefab", typeof(GameObject));
        if (newPrefab != null)
        {
            Debug.Log("Track prefab for " + trackname + " succesfully created in: "+ trackpath);
            EditorGUIUtility.PingObject(newPrefab);
        }

        //Delete copies in hierachy
        DestroyImmediate(trackPrefab);
        DestroyImmediate(trackData);

        //Render thumbnail image
        renderThumbnail(trackpath, trackname);

        UnityEditor.SceneManagement.EditorSceneManager.SaveScene(UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene(), UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().path);

    }

    private string prefabPath(string input) {

        //Remove numeric suffic from track name to get theme
        string theme = Regex.Replace(input, @"[\d-]", string.Empty);
        //Also remove "Arena" suffix
        theme.Replace("Arena", "");

        return "Assets/Resources/Tracks/" + theme + "/";
    }

    private void renderThumbnail(string path, string trackname)
    {
        //Application.CaptureScreenshot(path + trackname + " image.png", 1);
        ssCam = GameObject.Find("Main Camera").GetComponent<Camera>();

        RenderTexture rt = new RenderTexture(ssWidth, ssHeight, 24);
        ssCam.targetTexture = rt;

        Texture2D screenShot = new Texture2D(ssWidth, ssHeight, TextureFormat.RGB24, false);
        ssCam.Render();
        RenderTexture.active = rt;
        screenShot.ReadPixels(new Rect(0, 0, ssWidth, ssHeight), 0, 0);
        ssCam.targetTexture = null;
        RenderTexture.active = null;
        byte[] bytes = screenShot.EncodeToJPG();
        string filename = path + trackname + " image.jpg";

        System.IO.File.WriteAllBytes(filename, bytes);
        Debug.Log(string.Format("Took screenshot to: {0}", filename));

        //Application.OpenURL(filename);
    }
[/pane] [pane title=”LightmapDataBuilder” start=”closed”]
public void addComponents()
    {
        selected = Selection.activeTransform; //Root track object
        sceneRenderers = selected.GetComponentsInChildren<MeshRenderer>(false);


        if (!selected.GetComponent<LightmapLoader>())
        {
            Debug.LogError("Selected track does not have a LightmapLoader component!");
        }
        else
        {

            foreach (MeshRenderer r in sceneRenderers)
            {

                //Only objects with an active MeshRenderer
                if (r.enabled)
                {
                    //If doesnt have ObjectLightmapData component, add one
                    if (r.gameObject.GetComponent<ObjectLightmapData>() == null)
                    {
                        //Debug.Log("No ObjectLightmapData component found on " + r.gameObject.name);
                        r.gameObject.AddComponent<ObjectLightmapData>();
                    }

                    lightmapData = r.gameObject.GetComponent<ObjectLightmapData>();

                    if (lightmapData != null)
                    {
                        //Disable reflection probe usage, just in case, save performance
                        r.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off;
                        r.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;

						//Store current lightmap data in the ObjectLightmapData component
                        lightmapData.lightmapindex = r.lightmapIndex;
                        lightmapData.lightMapOffset = new Vector2(r.lightmapScaleOffset.z, r.lightmapScaleOffset.w);
                        lightmapData.lightMapTiling = new Vector2(r.lightmapScaleOffset.x, r.lightmapScaleOffset.y);
                    }
                    else
                    {
                        Debug.LogError(r.gameObject.name + ": LightmapData is null");
                    }

                }//if enabled

            }//foreach

        }//lightmaploader condition

    }
[/pane] [pane title=”ObjectLightmapData” start=”closed”]
[Header("Lightmap number")]
    public int lightmapIndex = 0;
    [Header("UV in lightmap")]
    public Vector2 lightMapTiling;
    public Vector2 lightMapOffset;
	
    private MeshRenderer renderer;

    void Start () {
        renderer = GetComponent<MeshRenderer>();
        renderer.lightmapIndex = lightmapIndex;
        renderer.lightmapScaleOffset = new Vector4(lightMapTiling.x, lightMapTiling.y, lightMapOffset.x, lightMapOffset.y);
    }
[/pane] [/accordion]

The graphics per track had various settings: light direction, post-effects (bloom, DoF, fog, colorgrading). However these were scattered between the directional light, camera components and scene lighting. For other artists I was working with this was potentially confusing. So gradually I starting unifying these parameters in an editor window. The setting shared by the track’s theme (desert, mountain, forest and snow) were saved in a preset file, to preserve consistency.

Graphics editor window

However, attributes belonging to the individual tracks were saved in a GraphicsLoader component on the root object. These parameters would then be assigned to the corresponding components upon initialization.

Effects

During a later period most, if not all of the work on the track was handed to the new art intern, so that I could focus on the visual effects. There has been a lot of iteration, so much of what I show here will not be in the final game, but in some other form.

Experimenting with creating procedural sprite, which worked incredibly well

“Low poly” effects, using a custom lighting shader

More to come! Game is still in development!

You may also like...

Leave a Reply

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