Skip to content

Recipe ‐ AR Slime Demo (Android)

Danielk0703 edited this page Jan 22, 2024 · 1 revision

Project Description

A simple demo project which utilizes Niantic Lightship SDK for automatic surface detection and navigation mesh creation.

You have to grant camera access on your smartphone for this demo to work.

Simply hold your smartphone and point it on a surface, preferably with a wall or corner for better detection results. After the surface has been created you can see blue squares. Touch with your finger on your smartphone on a square to send the agent (Slime) to the desired position.

Features

  • Create a navigation mesh on most surfaces
  • Navigate a friendly slime
  • Change the skin of you slime via the settings menu

Prerequisites

Accounts

Unity Version

  • 2022.3.15f1

  • Android Build Support

    • OpenJDK
    • Android SDK & NDK Tools

ProjectModules

Asset Packs

IDE

  • Rider 2023.x.x (or any newer version)
    • Please note that you may use any other IDE you are comfortable with

Installation Guides

Niantic Lightship Account Setup

  1. Sign in into your Niantic Lightship Account and create a New Project
  2. Click on Projects and create a New Project
  3. Change the project title from Untitled to AR Slime Demo
  4. Copy and save the API Key for later use temporarily in a text editor of your choice

AR Basic Setup

If anything changes in the future you might want to consider following the official Niantic Lightship Documentation.

  1. Create a new Unity 2022.3.15f1 project with the 3D (Core) template selected and name it AR Slime Demo, uncheck Unity Cloud and click on the Create Project button

CreateProject

  1. In your Unity project, open the Package Manager by selecting Window > Package Manager
    • From the plus menu on the left hand side in the Package Manager, select Add package from git URL...
    • Enter the git URL https://github.com/niantic-lightship/ardk-upm.git
    • Click Yes to activate the new Input System Package for AR Foundation 5.0 (if prompted)

PackageManager

  1. Close the package manager
  2. In the top menu of Unity, select Lightship > XR Plug-in Management to navigate to the XR Plug-in Management menu
  3. In the XR Plug-in Management menu, select the platform Android, then check the checkbox labelled Niantic Lightship SDK+ Google ARCore
    • Please note that in Unity version 2022.3.10f1 or above, you might see a benign error in the console at this point
  4. Close Project Settings
  5. Open the Lightship settings by selecting Lightship > Settings
  6. In the Project Settings window which just opened, paste your previously saved API Key into the API Key field under Credentials
  7. Close Project Settings
  8. Open the Build Settings window by selecting File > Build Settings
  9. Select Android, then click Switch Platform
  10. After the progress bar finishes, click Player Settings
  11. Player Settings for Android
    • Other Settings > Rendering - Uncheck Auto Graphics API
    • Other Settings > Rendering - Remove Vulkan from the Graphics API list
    • Other Settings > Minimum API Level - Set the Minimum API Level to Android 7.0 'Nougat' (API Level 24)
    • Other Settings > Scripting Backend - Select IL2CPP from the drop-down, then enable both ARMv7 and ARM64
  12. Close Project Settings

AR Project Setup

  1. Rename the scene SampleScene to MainScene
  2. Open the MainScene
  3. Delete the Main Camera game object from the Hierarchy panel
  4. Right click in the Hierarchy panel and select XR > AR Session
    • Set the position to 0, 0, 0
  5. Right click in the Hierarchy panel and select XR > XR Origin (Mobile AR)
    • Set the position to 0, 0, 0
  6. Save the scene by pressing Ctrl + S on your keyboard

Hierarchy1

  1. Navigate to File > Build Settings
  2. Press Add Open Scenes button to add the current scene to the build settings
  3. Close Build Settings

AR NavMesh Setup

ShadersAndMaterials Export

  1. Create folder Shaders

Add shader InvisibleMeshWithShadows

Shader "Unlit/InvisibleMeshWithShadows"
{
    Properties
    {
        _Shadowetensity ("Shadow Intensity", Range (0, 1)) = 0.6
    }
 
    SubShader
    {
 
        Tags {"Queue"="AlphaTest" }
 
        Pass
        {
            Tags {"LightMode" = "ForwardBase" }
            Cull Back
          

            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase
 
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"

            uniform float _Shadowetensity;
 
            struct v2f
            {
                float4 pos : SV_POSITION;  
                LIGHTING_COORDS(0,1)
            };
            v2f vert(appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos (v.vertex);
                TRANSFER_VERTEX_TO_FRAGMENT(o);
               
                return o;
            }
            fixed4 frag(v2f i) : COLOR
            {
                float attenuation = LIGHT_ATTENUATION(i);
                return fixed4(0,0,0,(1-attenuation)*_Shadowetensity);
            }
            ENDCG
        }
 
    }
    Fallback "VertexLit"
}

Add shader Silhouette

Shader "Custom/Silhouette" { 
	Properties {
		_Color ("Main Color", Color) = (1.0,1.0,1.0,1.0)
		_MainTex ("Base (RGB)", 2D) = "white" { }    
	}
 
    SubShader {
		Pass {
			Name "GHOST"
			Cull Back
			ZWrite Off           
			ZTest GEqual

            //if you want to remove the over draw for overlapping items add this stencil buffer.
            Stencil {
                Ref 1
                Comp GEqual
                Pass Invert
            }
            
            Blend SrcAlpha OneMinusSrcAlpha
	       
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
    
            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };
            
            struct v2f {
                float4 pos : POSITION;
                float4 color : COLOR;
                float2 uv : TEXCOORD0;
                
            };
            
            v2f vert(appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.color = float4(1,1,1,1);
            
                return o;
            }

            sampler2D _MainTex;
            
            half4 frag(v2f i) :COLOR {
                //can make these uniforms if you want it controllable.
                //setting color to 0 will make them a back silhouette, above mixes there colors.
                //setting alpha to 0 will remove the effect 1 will be a solid mix.
                float _AlphaAmount = 0.4;
                float _ColorAmount = 0.05;

                float4 c = tex2D(_MainTex, i.uv)*_ColorAmount;
                c.a=_AlphaAmount;
               return c;
            }
            ENDCG
		}
 
		Pass {
			Name "BASE"
			ZWrite On
			ZTest LEqual
            
			Blend SrcAlpha OneMinusSrcAlpha
               
            //not sure why we need this to be x2 but it works otherwise the texture is too dark some sort of colorspace thing
			Material {
                 Diffuse (2,2,2,1)           
			}
			Lighting On
			SetTexture [_MainTex] {
				constantColor [_Color]
				Combine texture  * constant
			}
	
		}
	}
 
	Fallback "Diffuse"
}
  1. Create folder Materials

Add material InvisibleMeshWithShadowsMat

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
  serializedVersion: 8
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_Name: InvisibleMeshWithShadowsMat
  m_Shader: {fileID: 4800000, guid: e9a4c4e1418b842fcbd3ef02ec087daa, type: 3}
  m_ValidKeywords: []
  m_InvalidKeywords: []
  m_LightmapFlags: 4
  m_EnableInstancingVariants: 0
  m_DoubleSidedGI: 0
  m_CustomRenderQueue: -1
  stringTagMap: {}
  disabledShaderPasses: []
  m_SavedProperties:
    serializedVersion: 3
    m_TexEnvs:
    - _BumpMap:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _DetailAlbedoMap:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _DetailMask:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _DetailNormalMap:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _EmissionMap:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _MainTex:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _MetallicGlossMap:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _OcclusionMap:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _ParallaxMap:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    m_Ints: []
    m_Floats:
    - _BumpScale: 1
    - _Cutoff: 0.5
    - _DetailNormalMapScale: 1
    - _DstBlend: 0
    - _GlossMapScale: 1
    - _Glossiness: 0.5
    - _GlossyReflections: 1
    - _Metallic: 0
    - _Mode: 0
    - _OcclusionStrength: 1
    - _Parallax: 0.02
    - _Shadowetensity: 0.6
    - _SmoothnessTextureChannel: 0
    - _SpecularHighlights: 1
    - _SrcBlend: 1
    - _UVSec: 0
    - _ZWrite: 1
    m_Colors:
    - _Color: {r: 1, g: 1, b: 1, a: 1}
    - _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
  m_BuildTextureStacks: []

Add material Silhouette

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
  serializedVersion: 8
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_Name: Silhouette
  m_Shader: {fileID: 4800000, guid: 768fecefaff8747a19bf3daa524caab7, type: 3}
  m_ValidKeywords: []
  m_InvalidKeywords:
  - _ALPHAPREMULTIPLY_ON
  m_LightmapFlags: 4
  m_EnableInstancingVariants: 0
  m_DoubleSidedGI: 0
  m_CustomRenderQueue: -1
  stringTagMap: {}
  disabledShaderPasses: []
  m_SavedProperties:
    serializedVersion: 3
    m_TexEnvs:
    - _BumpMap:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _DetailAlbedoMap:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _DetailMask:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _DetailNormalMap:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _EmissionMap:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _MainTex:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _MetallicGlossMap:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _OcclusionMap:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    - _ParallaxMap:
        m_Texture: {fileID: 0}
        m_Scale: {x: 1, y: 1}
        m_Offset: {x: 0, y: 0}
    m_Ints: []
    m_Floats:
    - _BumpScale: 1
    - _Cutoff: 0.5
    - _DetailNormalMapScale: 1
    - _DstBlend: 10
    - _GlossMapScale: 1
    - _Glossiness: 0.5
    - _GlossyReflections: 1
    - _Metallic: 0
    - _Mode: 3
    - _OcclusionStrength: 1
    - _Parallax: 0.02
    - _SmoothnessTextureChannel: 0
    - _SpecularHighlights: 1
    - _SrcBlend: 1
    - _UVSec: 0
    - _ZWrite: 0
    m_Colors:
    - _Color: {r: 0, g: 0.8310709, b: 1, a: 0.3529412}
    - _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
  m_BuildTextureStacks: []
  1. Create an empty game object with the name InvisibleMeshPrefab and set the position to 0, 0, 0
    • Add the component Mesh Filter
    • Add the component Mesh Renderer
      • Under Materials set InvisibleMeshWithShadowsMat
      • Under Lighting set Cast Shadows to Off
    • Add the component Mesh Collider

InvisibleMeshPrefab

  1. Create folder Prefabs
  2. Create a prefab from InvisibleMeshPrefab and add it into the Prefabs folder
  3. Remove the original object from the scene
  4. Create an empty game object under the Camera Offset object with the name Meshing

Hierarchy4

  1. Add the component ARMeshManager
    • Assign the previously created InvisibleMeshPrefab into Mesh Prefab

Meshing

  1. Create an empty game object with the name LightshipNavMeshManager and set the position to 0, 0, 0
    • Add the component Lightship Nav Mesh Manager
      • Assign the Main Camera to Camera
      • Set the Layer Mask to Default
    • Add the component Lightship Nav Mesh Renderer which is used for visualising the navigation mesh (For development purposes only)
      • Assign the LightshipNavMeshManager to Lightship Nav Mesh Manager
      • Assign the Material Silhouette to Material
  2. Save the scene by pressing Ctrl + S on your keyboard

LightshipNavMeshManager

Hierarchy2

AR NavMesh Agent Setup

  1. Create a new folder called Scripts

Assets1

  1. Create a new Script NavMeshAgentNavigator in Scripts folder
    • Add the following content to the newly created script
// NavMeshAgentNavigator.cs
using System.Collections;
using System.Collections.Generic;
using Niantic.Lightship.AR.NavigationMesh;
using UnityEngine;

public class NavMeshAgentNavigator : MonoBehaviour
{
    [Header("Agent Settings")]
    public float walkingSpeed = 3.0f;

    public float jumpDistance = 1;
    public int jumpPenalty = 2;

    public PathFindingBehaviour pathFindingBehaviour = PathFindingBehaviour.InterSurfacePreferResults;

    [Header("Scene References")]
    public LightshipNavMeshManager lightshipNavMeshManager;

    private enum AgentNavigationState
    {
        Paused,
        Idle,
        HasPath
    }

    private AgentNavigationState state = AgentNavigationState.Idle;
    private Path path = new(null, Path.Status.PathInvalid);

    private Coroutine actorMoveCoroutine;
    private Coroutine actorJumpCoroutine;

    private AgentConfiguration agentConfig;
    private LightshipNavMesh lightshipNavMesh;

    private bool agentReady;

    private IEnumerator Start()
    {
        agentConfig = new AgentConfiguration(jumpPenalty, jumpDistance, pathFindingBehaviour);

        yield return new WaitForSeconds(0.1f);
        lightshipNavMesh = lightshipNavMeshManager.LightshipNavMesh;
        agentReady = true;
    }

    private void Update()
    {
        if (!agentReady)
            return;

        switch (state)
        {
            case AgentNavigationState.Paused:
                break;

            case AgentNavigationState.Idle:
                StayOnNavMesh();
                break;

            case AgentNavigationState.HasPath:
                break;
        }
    }

    private void StopMoving()
    {
        if (actorMoveCoroutine != null)
            StopCoroutine(actorMoveCoroutine);
    }

    public void SetDestination(Vector3 destination)
    {
        StopMoving();

        if (lightshipNavMesh == null)
            return;

        lightshipNavMesh.FindNearestFreePosition(transform.position, out Vector3 startOnBoard);

        bool result = lightshipNavMesh.CalculatePath(startOnBoard, destination, agentConfig, out path);

        if (!result)
            state = AgentNavigationState.Idle;
        else
        {
            state = AgentNavigationState.HasPath;
            actorMoveCoroutine = StartCoroutine(Move(transform, path.Waypoints));
        }
    }

    private void StayOnNavMesh()
    {
        if (lightshipNavMesh == null || lightshipNavMesh.Area == 0)
            return;

        if (lightshipNavMesh.IsOnNavMesh(transform.position, 0.2f))
            return;

        List<Waypoint> pathToNavMesh = new();
        lightshipNavMesh.FindNearestFreePosition(transform.position, out Vector3 nearestPosition);

        pathToNavMesh.Add(new Waypoint
        (
            transform.position,
            Waypoint.MovementType.Walk,
            Utils.PositionToTile(transform.position, lightshipNavMesh.Settings.TileSize)
        ));

        pathToNavMesh.Add(new Waypoint
        (
            nearestPosition,
            Waypoint.MovementType.SurfaceEntry,
            Utils.PositionToTile(nearestPosition, lightshipNavMesh.Settings.TileSize)
        ));

        path = new Path(pathToNavMesh, Path.Status.PathComplete);
        actorMoveCoroutine = StartCoroutine(Move(transform, path.Waypoints));
        state = AgentNavigationState.HasPath;
    }

    private IEnumerator Move(Transform actor, IList<Waypoint> path)
    {
        Vector3 startPosition = actor.position;
        Quaternion startRotation = actor.rotation;
        float interval = 0.0f;
        int destIdx = 0;

        while (destIdx < path.Count)
        {
            Vector3 destination = path[destIdx].WorldPosition;

            //make sure the destination is on the mesh
            //LightshipNavMesh is an average height so can be under/over the mesh
            //the nav agent should always stand on the mesh, so using a ray cast to lift them up/down as they move.
            Vector3 from = destination + Vector3.up;
            Vector3 dir = Vector3.down;

            RaycastHit hit;

            if (Physics.Raycast(from, dir, out hit, 100, lightshipNavMesh.Settings.LayerMask))
            {
                destination = hit.point;
            }

            //do i need to jump or walk to the target point
            if (path[destIdx].Type == Waypoint.MovementType.SurfaceEntry)
            {
                yield return new WaitForSeconds(0.5f);

                actorJumpCoroutine = StartCoroutine
                (
                    Jump(actor, actor.position, destination)
                );

                yield return actorJumpCoroutine;

                actorJumpCoroutine = null;
                startPosition = actor.position;
                startRotation = actor.rotation;
            }
            else
            {
                //move on step towards target waypoint
                interval += Time.deltaTime * walkingSpeed;
                actor.position = Vector3.Lerp(startPosition, destination, interval);
            }

            //face the direction we are moving
            Vector3 lookRotationTarget = (destination - transform.position);

            //ignore up/down we dont want the creature leaning forward/backward.
            lookRotationTarget.y = 0.0f;
            lookRotationTarget = lookRotationTarget.normalized;

            //check for bad rotation
            if (lookRotationTarget != Vector3.zero)
                transform.rotation = Quaternion.Lerp(startRotation, Quaternion.LookRotation(lookRotationTarget),
                    interval);

            //have we reached our target position, if so go to the next waypoint
            if (Vector3.Distance(actor.position, destination) < 0.01f)
            {
                startPosition = actor.position;
                startRotation = actor.rotation;
                interval = 0;
                destIdx++;
            }

            yield return null;
        }

        actorMoveCoroutine = null;
        state = AgentNavigationState.Idle;
    }

    private IEnumerator Jump(Transform actor, Vector3 from, Vector3 to, float speed = 2.0f)
    {
        float interval = 0.0f;
        Quaternion startRotation = actor.rotation;
        float height = Mathf.Max(0.1f, Mathf.Abs(to.y - from.y));
        while (interval < 1.0f)
        {
            interval += Time.deltaTime * speed;
            Vector3 rotation = to - from;
            rotation = Vector3.ProjectOnPlane(rotation, Vector3.up).normalized;
            if (rotation != Vector3.zero)
                transform.rotation = Quaternion.Lerp(startRotation, Quaternion.LookRotation(rotation), interval);
            Vector3 p = Vector3.Lerp(from, to, interval);
            actor.position = new Vector3
            (
                p.x,
                -4.0f * height * interval * interval +
                4.0f * height * interval +
                Mathf.Lerp(from.y, to.y, interval),
                p.z
            );

            yield return null;
        }

        actor.position = to;
    }
}
  1. Create a new Script InputManager in Scripts folder
    • Add the following content to the newly created script
// InputManager.cs
using UnityEngine;

public class InputManager : MonoBehaviour
{
    [Header("Scene References")]
    public NavMeshAgentNavigator navMeshAgentNavigator;
    
    private void Update()
    {
        if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Ended)
        {
            // Create ray from the camera and passing through the touch position
            Ray ray = Camera.main.ScreenPointToRay(Input.GetTouch(0).position);
            HandleTouchInput(ray);
        }

        if (Input.GetMouseButtonUp(0))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            HandleTouchInput(ray);
        }
    }

    private void HandleTouchInput(Ray ray)
    {
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit) && !UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject())
        {
            navMeshAgentNavigator.SetDestination(hit.point);
        }
    }
}
  1. Create an empty game object with the name NavMeshAgent and set the position to 0, 0, 2
    • Add the component NavMeshAgentNavigator
      • Assign the LightshipNavMeshManager to Lightship Nav Mesh Manager

NavMeshAgent

  1. Create a Cube as a child of NavMeshAgent with the name Mesh and set the scale to 0.1, 0.1, 0.1
    • Remove the Box Collider component

Mesh

  1. Create a new material with the name Nose in Materials folder
    • Change to color to #FF4949
  2. Create a Cube as a child of Mesh with the name Nose, set the position to 0, 0, 0.5 and the scale to 0.2, 0.2, 0.3
    • Remove the Box Collider component
    • Assign the Nose material

Nose

DemoAgent

  1. Create an empty game object with the name - Managers - and set the position to 0, 0, 0
    • Add the component Input Manager
      • Assign the NavMeshAgent to NavMeshAgentNavigator

Managers

  1. Create a Cube with the name Floor, set the position to 0, -1, 1.5 and the scale to 5, 0.1, 5

Floor

  1. In the Hierarchy right click and click on UI > Canvas to create an new UI Canvas and the EventSystem
  2. Rearrange the game objects in the Hierarchy for a better overview

Hierarchy3

  1. Change the XR Origin rotation to 14.5, 0, 0 to be able to see the floor easier
  2. Press Play in Unity and check out the demo
    • The NavMeshAgent should automatically place themself on the NavMesh and if you click on a random point on the screen / the NavMesh the NavMeshAgent should automatically move to the nearest possible point
  3. Save the scene by pressing Ctrl + S on your keyboard
  4. You might see some z-fighting if you want to fix this, just change the Size of the Box Collider of the Floor to 1, 1.01, 1

Scene

  1. Save the scene by pressing Ctrl + S on your keyboard
  2. You can now deploy it to your smartphone (Android) and try it out
    • Be sure to disable or remove the Floor game object before deployment otherwise it might cover the screen
  3. You are now good to go and continue on your own or if you want to have a different character as well as UI interactions continue with the next section

Prettify

  1. Add the Kawaii Slimes asset pack to the project
  2. Create folder Plugins
  3. Move the folder Kawaii Slimes into Plugins
  4. Copy Prefabs from Kawaii Slimes Plugin
    • Navigate to the Plugins/Kawaii Slimes/Prefabs folder in the Project panel.
    • Select all prefab files within the Plugins/Kawaii Slimes/Prefabs folder
    • Right-click on the selected prefabs and choose Copy
    • Navigate to the Prefabs folder in the Project panel and Paste the Prefabs here
  5. Add Animators to Each Slime Prefab
    • In the Prefabs folder, select all the Kawaii Slime prefabs
    • In the Inspector panel, click on the Add Component button
    • Search for Animator and select it to add an Animator component to the prefabs
    • Now set the Controller to Slime and the Avatar to Slime_AnimAvatar

PrefabSelection

  1. In the Hierarchy panel, select the Canvas game object
    • Right-click on Canvas and navigate to UI > Raw Image to create a new Raw Image under the Canvas
    • With the Raw Image selected, go to the Inspector panel
    • Rename the Raw Image to UI Menu Open Button
    • In the Rect Transform component, click on the Anchor Preset button and select the top-right anchor preset to set the Raw Image's anchor to the top right corner
    • Position the Raw Image in the Top Right Corner
    • With the Raw Image still selected in the Hierarchy panel, click on the Add Component button in the Inspector panel
    • Search for Button and select it to add a Button component to the Raw Image
    • If you have a Icon that you want to use for the Button you can set it as the Texture in the Raw Image

UIMenuOpenButton

  1. In the Hierarchy panel, right-click on the Canvas game object and navigate to UI > Image to create a new image
    • Rename the newly created image to UI Menu Container
  2. Right-click on the UI Menu Container image in the Hierarchy and go to UI > Image to add another image as a child
  3. Rename this child image to Menu Background
  4. Adjust the alpha value of the UI Menu Container to 130 for semi-transparency and set the color to black
  5. Select the Menu Background in the Hierarchy and in the Inspector panel, set the color to a dark gray
  6. Set Anchor for UI Menu Container and Menu Background to Stretch
  7. For the UI Menu Container set Left, Right, Top, and Bottom fields all to 0 under the Rect Transform component

UIMenuContainer

  1. For the Menu Background set Left, Right, Top, and Bottom fields all to 100 under the Rect Transform component

MenuBackground

  1. Select the Menu Background in the Hierarchy panel
  2. Right-click on it and navigate to UI > Text - TextMeshPro to create a new Text Mesh Pro text object (move the created TextMesh Pro Folder to the Plugins Folder)
  3. Rename this new object to Menu Title
  4. Right-click on the Menu Background again and go to UI > Dropdown - TextMeshPro
  5. Rename this new object to Menu Dropdown
  6. Right-click on the Menu Background and select UI > Raw Image
  7. Rename this new Raw Image to Slime Preview
  8. Right-click on the Menu Background and choose UI > Button
  9. Rename the new button to Continue Button
  10. Customize these UI elements to how you want them to look

Menu1

  1. Select the Continue Button in the Hierarchy panel
  2. In the Inspector panel, scroll down to the Button component where you'll find the OnClick() event section
  3. Click the + button to add a new event
  4. Drag the UI Menu Container from the Hierarchy panel and drop it into the object field of the newly created event
  5. In the function dropdown (No Function selected by default), navigate to GameObject > SetActive
  6. Leave the checkbox next to the function set to false

ButtonEvent

  1. Select the UI Menu Open Button in the Hierarchy panel and do the same as you did for the Continue Button but set the checkbox to true
  2. In the Unity Project panel, right-click in the Assets directory - select Create > Folder and name this new folder Textures
  3. Navigate into the Textures folder you just created
  4. Right-click within the folder, select Create > Render Texture
  5. A new render texture will be created in the Textures folder rename it to Slime Preview Texture
  6. Assign this Texture to the Slime Preview Image in the Menu Background
  7. Select the UI Menu Container in the Hierarchy panel
  8. Right-click on it and choose Create Empty to create a new empty GameObject
  9. With the new empty GameObject selected, in the Inspector panel, right-click on the Rect Transform component and select Remove Component to remove it
  10. Rename this GameObject to Preview Slime Box
  11. With Preview Slime Box selected, right-click on it and choose 3D Object > Cube to create a new cube
  12. Rename this cube to Wall and adjust its position and scale to serve as a wall
  13. Create another cube following the same steps and rename it to Floor
  14. Right-click on Preview Slime Box and choose Camera to add a new camera to this GameObject
  15. Rename the camera as needed
  16. Assign the Render Texture (created previously in the Textures folder) to the Target Texture field of the camera
  17. Also remove the Audio Listener from the created Camera
  18. The Camera should look at the 0,0,0 local position so that when a new object is created within the Preview Slime Box it is in the center of the Render Texture
  19. In the Project panel open the Materials folder, right-click and navigate to Create > Material to create a new material
  20. Rename the newly created material to a descriptive name, such as Slime Preview Background
  21. Select the new material to view its properties in the Inspector panel in the Shader dropdown menu, select Unlit > Color
  22. Set the color of the material to match the background color of your menu

PreviewBox

  1. In the Hierarchy panel, locate and select the -Managers- GameObject
  2. In the Inspector panel, click on the Add Component button
  3. In the search bar that appears, type CharacterManager and create a new Script to add it to the -Managers- GameObject
// CharacterManager.cs
using System.Collections.Generic;
using TMPro;
using UnityEngine;

public class CharacterManager : MonoBehaviour {
    public TMP_Dropdown characterSelectDropdown;

    public GameObject slimePreviewBox;
    public GameObject navMeshAgentParent;
    
    public List<GameObject> characterPrefabs = new();

    private GameObject agentMesh;
    private GameObject previewSlimeMesh;

    private void Start() {
        SetDropdownOptions();

        // Add listener for the Dropdown value changes
        characterSelectDropdown.onValueChanged.AddListener(delegate { DropdownValueChanged(); });

        // Set Current selection
        DropdownValueChanged();
    }

    private void DropdownValueChanged() {
        // Get the current index
        int index = characterSelectDropdown.value;

        // Destroy old Objects
        if (agentMesh)
            Destroy(agentMesh);

        if (previewSlimeMesh)
            Destroy(previewSlimeMesh);

        // Instantiate new Objects
        previewSlimeMesh = Instantiate(characterPrefabs[index], slimePreviewBox.transform);
        agentMesh = Instantiate(characterPrefabs[index], navMeshAgentParent.transform);

        // Set Position and disable shadows
        previewSlimeMesh.transform.localPosition = Vector3.zero;
        previewSlimeMesh.GetComponentInChildren<SkinnedMeshRenderer>().receiveShadows = false;

        // Set Position and scale
        agentMesh.transform.localPosition = Vector3.zero;
        agentMesh.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);
    }


    private void SetDropdownOptions() {
        // Clear current options
        characterSelectDropdown.ClearOptions();

        // Check if the list is not empty
        if (characterPrefabs != null && characterPrefabs.Count > 0) {
            // Create a list to hold the names
            List<string> options = new List<string>();

            // Loop through the GameObjects and add their names to the list
            foreach (GameObject gameObject in characterPrefabs) {
                options.Add(gameObject.name.Replace('_', ' '));
            }

            // Add the list of names as options
            characterSelectDropdown.AddOptions(options);
        }
    }
}
  1. With the Character Manager component now added, you will see various fields in the Inspector panel
  2. Assign all the necessary objects to the script and it should look like this in the end

CharacterManager

  1. Save the scene by pressing Ctrl + S on your keyboard

Menu2

Deployment

  1. Open the Build Settings window by selecting File > Build Settings
  2. Click Build and select a destination folder for the apk file
  3. Install the apk file to your Android device

Final Results

Now you have a project which will automatically detect surfaces and create navigation meshes on them which can be used by any type of NavMesh Agent.

You can download the build demo on itch.io.

Screenshots

Default Game View

Character Selection View

Game View with changed Character

Next Steps

  • In addition to the core mechanics you could add the possibility of throwing objects like food to the position where the slime should go and let the slime “consume” the object
  • Introduce a hunger level and adjust the color of the slime depending on it
  • Change the color of the tile where the slime is currently positioned
  • Make use of the different animations already provided with the slime
  • Add post processing to the scene to make it more pretty
  • Add particle effects to the slime to make it more pretty