0

How can I instantiate the desired number of prefabs using the following code? I need to spawn ONE (and only one) player prefab, X enemies and ONE (and only one) end game prefab.

    private void GenerateEnemies(int xMax, int zMax)
    {
        GameObject landscape = new GameObject("ENEMIES");


        for (int z = 0; z < zMax; z++)
        {
            for (int x = 0; x < xMax; x++)
            {
                randNum = Random.Range(0, 100);

                if (randNum < 10 )
                {
                    Instantiate(enemy1, new Vector3(x * 10, 0, z * 10), Quaternion.Euler(0, 0, 0));//, landscape.transform);
                }
                else if (randNum < 20)
                {
                    Instantiate(enemy2, new Vector3(x * 10, 0, z * 10), Quaternion.Euler(0, 0, 0));//, landscape.transform);
                }

                else if (randNum < 30)
                {
                    Instantiate(enemy3, new Vector3(x * 10, 0, z * 10), Quaternion.Euler(0, 0, 0));//, landscape.transform);
                }

                else if (randNum < 40)
                {
                    Instantiate(enemy4, new Vector3(x * 10, 0, z * 10), Quaternion.Euler(0, 0, 0));//, landscape.transform);
                }

                else if (randNum < 50)
                {
                    Instantiate(enemy5, new Vector3(x * 10, 0, z * 10), Quaternion.Euler(0, 0, 0));//, landscape.transform);
                }

            }
        }
    }
  • `Random.Range(0, 100)` with an if-block-chain only handles values less than 50. What did you expect? Such an if-block-chain is also not good code, there's better ways of doing that (such as the random-by-index derHugo posted or a [Weighted List system](https://stackoverflow.com/q/1761626/1663383)). – Draco18s no longer trusts SE Sep 09 '19 at 13:57

2 Answers2

2

Well simply do your one-time things outside the loop!

Also

randNum = Random.Range(0, 100);

and then you use only 5 different cases and only if the value is smaller 50 (so about in half of the cases nothing happens at all ...). If this was intended .. ok-ish .. otherwise I would rather use a list and random indices:

// HINT: Rather have a list for your prefabs
// this shrinks your code a lot
public List<GameObject/*or whatever type*/> eneymPrefabs = new List<GameObject>();
public Gamebject playerPrefab;
public GameObject endGamePrefab;

private void GenerateEnemies(int xMax, int zMax)
{
    var landscape = new GameObject("ENEMIES");

    // Do these only once
    // store the references in case you need them later
    var player = Instantiate(playerPrefab);
    var endGame = Instantiate(endGamePrefab);

    for (int z = 0; z < zMax; z++)
    {
        for (int x = 0; x < xMax; x++)
        {
            // simply pick a random index from the prefab list
            int randIndex = Random.Range(0, eneymPrefabs.Count);

            // and get the according random prefab
            var enemyPrefab = enemyPrefabs[randIndex];

            if(enemyPrefab) Instantiate(enemyPrefab, new Vector3(x * 10, 0, z * 10), Quaternion.identity /*, landscape.transform*/);
        }
    }
}

Or an example for the weighted list mentioned by Draco18s

[Serializable]
public class WeightedPrefab
{
    public GameObject Prefab;
    public int Weight = 1;
}

public List<WeightedPrefab> weightedEnemyPrefabs;
public Gamebject playerPrefab;
public GameObject endGamePrefab;

private void GenerateEnemies(int xMax, int zMax)
{
    // create a temp list using the weights and random index on this one
    var enemyPrefabs = new List<GameObject>();
    foreach(var item in weightedEnemyPrefabs)
    {
        for(var i = 0; i < item.Weight; i++)
        {
            enemyPrefabs.Add(item.Prefab);
        }
    }

    // Rest stays the same

    var landscape = new GameObject("ENEMIES");

    // Do these only once
    // store the references in case you need them later
    var player = Instantiate(playerPrefab);
    var endGame = Instantiate(endGamePrefab);

    for (int z = 0; z < zMax; z++)
    {
        for (int x = 0; x < xMax; x++)
        {
            // simply pick a random index from the prefab list
            int randIndex = Random.Range(0, eneymPrefabs.Count);

            // and get the according random prefab
            var enemyPrefab = enemyPrefabs[randIndex];

            if(enemyPrefab) Instantiate(enemyPrefab, new Vector3(x * 10, 0, z * 10), Quaternion.identity /*, landscape.transform*/);
        }
    }
}

If it was intentional that not in every case an enemy is instantiated you can still use both approaches and simply leave a prefab reference empty &rightarrow; for that index nothing will be instantiated.


the enemy and end game should be part of the grid

In this case I would first write the entire grid combinations into a list. Pick two random entries from this list and place the player and the endGame there. Then block these two grid positions and do not spawn enemies there:

[Serializable]
public class WeightedPrefab
{
    public GameObject Prefab;
    public int Weight = 1;
}

public List<WeightedPrefab> weightedEnemyPrefabs;
public Gamebject playerPrefab;
public GameObject endGamePrefab;

private void GenerateEnemies(int xMax, int zMax)
{
    // Create a list of all awailable grid positions
    var gridPositions = new List<Vector2Int>();
    for (int z = 0; z < zMax; z++)
    {
        for (int x = 0; x < xMax; x++)
        {
            gridPositions.Add(new Vector2Int(x,z));
        }
    }

    // pick the two random positions for player and endgame
    var playerPosIndex = Random.Range(0, gridPositions.Count);
    var playerPos = gridPositions[playerPosIndex];
    gridPositions.RemoveAt(playerPosIndex);

    var endGamePosIndex = Random.Range(0, gridPositions.Count);
    var endGamePos = gridPositions[endGamePosIndex];
    gridPositions.RemoveAt(endGamePosIndex);

    // create a temp list using the weights and random index on this one
    var enemyPrefabs = new List<GameObject>();
    foreach(var item in weightedEnemyPrefabs)
    {
        for(var i = 0; i < item.Weight; i++)
        {
            enemyPrefabs.Add(item.Prefab);
        }
    }

    var landscape = new GameObject("ENEMIES");

    // Do these only once
    // store the references in case you need them later
    var player = Instantiate(playerPrefab, new Vector3(payerPos.x * 10, 0, playerPos.y * 10), Quaternion.identity /*, landscape.transform*/);
    var endGame = Instantiate(endGamePrefab, new Vector3(endGamePos.x * 10, 0, endGamePos.y * 10), Quaternion.identity /*, landscape.transform*/);

    for (int z = 0; z < zMax; z++)
    {
        for (int x = 0; x < xMax; x++)
        {
            // Now simply ignore the playerPos and endGamePos
            if(x == playerPos.x && z == playerPos.y) continue;
            if(x == endGamePos.x && z == endGamePos.y) continue;

            // pick a random index from the prefab list
            int randIndex = Random.Range(0, eneymPrefabs.Count);

            // and get the according random prefab
            var enemyPrefab = enemyPrefabs[randIndex];

            // do nothing if enemyPrefab is null otherwise instantiate
            if(enemyPrefab) Instantiate(enemyPrefab, new Vector3(x * 10, 0, z * 10), Quaternion.identity /*, landscape.transform*/);
        }
    }
}
derHugo
  • 83,094
  • 9
  • 75
  • 115
  • thanks for your replies. I prefer not using the list approach, I've already tried it but then I realized it's kind of a repetition of my loops, because I use the xMax and zMax system to generate both my list and the special positioning (correct me if i'm wrong). for instance if I use the suggested `weightedEnemyPrefabs` and `foreach` approach, how could I integrate it with my `xMax, zMax` loop which already does a list? –  Sep 09 '19 at 20:02
  • sorry I don't understand your concerns about the List approach. As commented we don't really get why you are using a random range between 0-99 and then have only cases for `< 50` ... `xMax, zMax` I wouldn't call this a list but rather a grid. As far as I understand you want one enemy object spawned on each of these grid spots. Or did you mean the player and endGame should be part of this grid as well? – derHugo Sep 09 '19 at 20:09
  • the enemy and end game should be part of the grid, yes. that's because I'm using the same xMax, zMax to generate my game level, by spawning a floor tile on each position. i'll try anyway your approach and let you know –  Sep 10 '19 at 13:35
  • I guess you mean the player and endGame since the enemies are already in the grid ;) – derHugo Sep 10 '19 at 14:04
  • lol, yes sorry. anw i tried the script in game and i saw if the the player is spawned as a prefab it loses all the other scripts (which are not attached to it directly) such as Xp tetxt, life point text etc. so i think i will not spawn the player as a prefab. i'll find another solution –  Sep 10 '19 at 16:12
  • well instead of spawning you can also just take an already existing object and only move it to the according `transform.position` – derHugo Sep 10 '19 at 16:14
  • cool, i'll try that. btw i have some problems with your last code: `var List gridPositions = new List(); gives me error and I cannot create the var. also I had to add `System.serializable` at the beginning and `private void GenerateEnemies(int xMax, int zMax)` in my method. is that correct? ` –  Sep 10 '19 at 16:28
  • remove the `List` .. `var` already "knows" its type ;) I guess you mean you have to add `using System;` to the top of your script and yes without the `[Serializable]` attribute you wouldn't be able to adjust things in the Unity Inspector. The last part yes sorry .. I was going to shrink the entire code down to only show the changes but I noticed this wasn't that clear so I added the whole implementation again but forgot to put in `int xMax, int zMax` again – derHugo Sep 10 '19 at 16:35
  • I tried your solution. it works but endgame prefab now spawns in a not-unique spot and overlaps other prefabs. I guess it's because they're out of xMax loop. this was my original problem when I started using the script suggested by unity (using InvokeRepeating). https://learn.unity.com/tutorial/survival-shooter-training-day-phases?projectId=5c514921edbc2a002069465e#5c7f8528edbc2a002053b721 –  Sep 10 '19 at 17:00
  • I managed to make it work following derHugo tips. everything is working correctly now as long as I set both the plyer and the end game obj from the inspector. thanks for your patience :) –  Sep 10 '19 at 20:20
0

Consider this code:

 for (int z = 0; z < zMax; z++)
 {
   for (int x = 0; x < xMax; x++)
   {
     randNum = Random.Range(1, 11);

     if (randNum == 1) Instantiate(enemy1, new Vector3(x * 10, 0, z * 10), Quaternion.Euler(0, 0, 0));//, landscape.transform);
     if (randNum == 2) Instantiate(enemy2, new Vector3(x * 10, 0, z * 10), Quaternion.Euler(0, 0, 0));//, landscape.transform);
     if (randNum == 3) Instantiate(enemy3, new Vector3(x * 10, 0, z * 10), Quaternion.Euler(0, 0, 0));//, landscape.transform);
     if (randNum == 4) Instantiate(enemy4, new Vector3(x * 10, 0, z * 10), Quaternion.Euler(0, 0, 0));//, landscape.transform);
     if (randNum == 5) Instantiate(enemy5, new Vector3(x * 10, 0, z * 10), Quaternion.Euler(0, 0, 0));//, landscape.transform);
   }
 }

It should be clear this code does the same thing your posted code does.

It should also be clear this is better because it will always run at the same speed (often bad in games to have it randomly run slower)

Hogan
  • 69,564
  • 10
  • 76
  • 117
  • `It should also be clear this is better because it will always run at the same speed` .. how that .. where is this anyhow faster then OP's code? You probably want to use `==` for comparisons? And `switch-case` would be more performant here. Also note that the second parameter of `Random.Range` is exclusive for `int` so this returns values `0-9` while in OP's code it was `0-99` .. this gives slightly different probabilities – derHugo Sep 10 '19 at 17:10
  • @derHugo -- good points -- my point was that this would always run at the same speed -- not that it would run faster. fastest would be a lookup array, not switch. – Hogan Sep 10 '19 at 20:17