Home > Software design >  Unity - How to make the player spawn on procedural terrain?
Unity - How to make the player spawn on procedural terrain?

Time:01-08

I'm making a procedurally generated world for my Unity game, and I want to make the player spawn in a random location but also always spawn on top of the terrain without falling through the ground. How can I do this?

I tried using this code below to make a raycast that will collide with the terrain and if the raycast can collide with the terrain it will spawn the player. However, the player will still sometimes fall through the ground.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerSpawn : MonoBehaviour
{
     public GameObject player;

     public void SpawnPlayer() {
   
     for (int i = 0; i < 1; i  ) {

        float sampleX = Random.Range(-350, 350);
        float sampleY = Random.Range(-350, 350);
        Vector3 rayStart = new Vector3(sampleX, 150, sampleY);
        
        if (!Physics.Raycast(rayStart, Vector3.down, out RaycastHit hit, Mathf.Infinity))
            continue;
        
        GameObject spawnedPlayer = Instantiate(player, transform);
        spawnedPlayer.transform.position = new Vector3(hit.point.x, Random.Range(35, 300), hit.point.z);
        spawnedPlayer.transform.Rotate(Vector3.up, Random.Range(0, 360), Space.Self);
        spawnedPlayer.transform.rotation = Quaternion.Lerp(transform.rotation, transform.rotation * Quaternion.FromToRotation(spawnedPlayer.transform.up, hit.normal), 0);
        Debug.Log("Spawned Player");
     }
}

public void SpawnCheck() {
    float sampleX = Random.Range(-350, 350);
    float sampleY = Random.Range(-350, 350);
    Vector3 rayStart = new Vector3(sampleX, 150, sampleY);

    if (!Physics.Raycast(rayStart, Vector3.down, out RaycastHit hit, Mathf.Infinity))
        StartCoroutine("spawn");
    else
        SpawnPlayer();
}

 public IEnumerator spawn() {
    yield return new WaitForSeconds(.25f);
    SpawnCheck();
}

void Start()
{
    StartCoroutine("spawn");
}

}

CodePudding user response:

In Unity the Y axis is usually the up and down axis. In your code you are setting the X and Z position using hit.point.x and hit.point.z but you are randomly selecting the Y axis with Random.Range(35, 300) which may randomly set your player under your terrain.

You should use hit.point.y because that should be where the raycast hit your terrain on the Y axis. You may need to add a little extra offset to Y as well to adjust for the player's height too make sure the player's collision is above the terrain.

spawnedPlayer.transform.position = new Vector3(hit.point.x, hit.point.y   someOffset, hit.point.z);

CodePudding user response:

There's a few little issues with your code.

These are the assumptions I've made:

  • it looks like you want to initially wait half a second after the game starts for the player to spawn.
  • it would seem that you want to player to spawn a random height off the terrain floor.
  • it looks like you want your spawned player to align to the terrain normal, as well as rotate to face a random direction as well.

The first issue is the flow of the code. You cast a ray to check the terrain. If there's a hit, you then call SpawnPlayer which then does a completely brand new raycast again. If SpawnPlayer fails to spawn the player, the code doesn't continue anywhere, and you end up with no player being spawned.

The next issue, the one directly in relation to your question, is that even when you find a valid spawn position, you then choose a new random height at which to spawn. This line:

spawnedPlayer.transform.position = new Vector3(hit.point.x, Random.Range(35, 300), hit.point.z);

places your player in the right X,Z location on the plane, but then selects a random Y position (height) which might be underneath the terrain.

Based on my assumptions, here's some updated code that I think is what you're looking for:

public class PlayerSpawn : MonoBehaviour
{
    public const float TERRAIN_X_MIN = -350;
    public const float TERRAIN_X_MAX = 350;
    public const float TERRAIN_Z_MIN = -350;
    public const float TERRAIN_Z_MAX = 350;
    public GameObject player;

    IEnumerator Start ( )
    {
        yield return new WaitForSeconds ( .25f );
        SpawnPlayer ( );
    }

    public void SpawnPlayer ( )
    {
        var safetyCount = 0;
        var safetyLimit = 100;
        do
        {
            var rayStart = new Vector3(Random.Range(TERRAIN_X_MIN, TERRAIN_X_MAX), 150, Random.Range(TERRAIN_Z_MIN, TERRAIN_Z_MAX));
            if ( !Physics.Raycast ( rayStart, Vector3.down, out var hit, Mathf.Infinity ) )
                continue;
                
            var spawnedPlayer = Instantiate(player, transform).transform;
            spawnedPlayer.position = new Vector3 ( hit.point.x, hit.point.y   Random.Range ( 35, 300 ), hit.point.z );
            spawnedPlayer.rotation *= Quaternion.FromToRotation ( Vector3.up, hit.normal );
            spawnedPlayer.Rotate ( spawnedPlayer.up, Random.Range ( 0, 360 ), Space.World );

            Debug.Log ( "Spawned Player" );
            return;
        } while ( true && safetyCount   < safetyLimit );
        Debug.LogWarning ( $"Could not spawn player after {safetyLimit} attempts. Are your ray start values correct?" );
    }
}

And this is the result:

spawned player animation

This is far from a complete solution, and is based on a few assumptions, but hopefully gets you going in the right direction.

  • Related