Alright, Im working in Unity in C# and Ive created a few simple functions to spawn a series of platforms (game objects) indefinitely in accordance with the z position of the player. I delete the game objects once they are securely out of sight and called this update method every time player moves forward:
void spawnSectorGroup()
{
int i = numSectorsToSpawn; //get position of last sectors, when its exceeded by player then delete and spawn again
while (i >= 0) {
int j = Random.Range (0, sectors.Length);
spawnSector (gamePosition.position, sectors [j]);
i--;
}
}
void checkAndUpdateSectors()
{
//print ("Player pos"+playerPosition.position);
//print ("last sector"+gamePosition.position);
if (gamePosition.position.z - playerPosition.position.z <= 20) { //safe value ahead
print ("theyre almost there, spawn new group");
print (gamePosition.position.z - playerPosition.position.z);
spawnSectorGroup ();
}
//destroy past ones
GameObject[] objects = FindObjectsOfType(typeof(GameObject)) as GameObject[];
//GameObject[] objects = GameObject.FindGameObjectsWithTag("Block");
foreach (GameObject o in objects) {
if (o.gameObject.tag == "Block" || o.gameObject.tag == "Cage" || o.gameObject.tag == "Animal" || o.gameObject.tag == "Enemy") {
if (playerPosition.position.z - o.transform.position.z >= 100) { //safe aways back
print ("destroying past object");
print (o.transform.position);
Destroy (o.gameObject);
}
}
}
}
void spawnSector(Vector3 relativePosition,GameObject sector){
GameObject newSector = GameObject.Instantiate (sector, relativePosition, transform.rotation) as GameObject;
gamePosition.position += newSector.GetComponent<Sector> ().length;
}
This all works, however Im worried in the long run if this spawning of about 10 new "sectors" each time the player is within 20 or so units of the last spawned sector will build up and create a lag from the multitude of game objects.
Im not experienced with indefinite spawning - will this be a problem? Or is this good practice with indefinite spawning?
Creating and destroying objects is expensive and something you want to avoid doing in large amounts while your game is running.
Check out this Unity tutorial on Object Pooling. The basic idea is that instead of creating and destroying the objects, you take them from a pool of existing objects and return them when you are done so they can be reused.
Related
Context
Hello, currently creating a clone of "Crossy Road" and what I'm trying to do is to spawn my moving object called "Vehicle" at a random speed and rate of spawn. This is also applicable to "Plank", but I will start first with the vehicle. So far, everything is working fine as intended for the game mechanics, but I would like to finalize with this issue so it is fully functional in terms of playability.
Problem
My issue now is I 3 different spawns objects: grass, river, and road. Each object holds other objects (let's call it spawners) depending of what field is being spawn. For example, if grass field object is spawned, it will spawn trees depending in a random varied selection. Another example is with road field. When the road is spawned, a vehicle will be spawned from either left or right in its current initial position. This vehicle will moves as intended with a random speed, but not with the original spawn position and rate (as shown in the GIF. The vehicle spawns in the middle of the road and not in the beginning of the left/right road).
As far I'm aware, my rate is currently unused because it is not the main issue I want to solve. However, the issue now is with the transform position not working as I have pictured in my head. So what is happening is that when the road is spawned again, the vehicle is spawned in the middle of the trajectory instead of resetting to the beginning.
Also, I have noticed that when I print the vehicle object, the Z-axis has a weird number compared to the original position.
Attempts done
I have been thinking that maybe it is the way I have set everything up. I have 4 vehicle objects with a child object called "Tank". However, in each vehicle object, I'm using SetActive(...) only and not really reusing the object itself to the beginning. Later on, I want to organize this spaghetti code and optimize it (e.g ObjectPool to spawn my roads and other GameObjects after hitting a certain range, adding a player range detection to spawn a field to name a few).
To be honest, my whole code feels bloated for something simple. This will be fixed once everything is working accordingly.
Code (DISCLAIMER: there is the possibility that there are unused variables)
SpawnManager.cs (some links provided too from learning to make this)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/*
** Weighted randomness: https://forum.unity.com/threads/random-numbers-with-a-weighted-chance.442190/
** Scriptable Object Weight spawn example: https://www.youtube.com/watch?v=FCksj9ofUgI&ab_channel=LlamAcademy
** From scratch loot tables with Scriptable Objects to make a loot table: https://www.youtube.com/watch?v=tX3RWsVLnzM&ab_channel=GregDevStuff
** Creating a random with an animation curve: https://www.youtube.com/watch?v=zw1OERK5xvU&ab_channel=HamzaHerbou
** Random Vehicle position spawn (maybe this can help me): https://stackoverflow.com/questions/51312481/move-spawn-object-to-random-position
*/
public class SpawnManager : MonoBehaviour
{
public GameObject Player;
public Spawn[] Field;
public GameObject[] SpawnObjectTrees;
public GameObject[] SpawnObjectVehicles; //different vehicles
public GameObject[] SpawnObjectPlanks; //3 sizes (small, medium, large)
private PlayerControl2 playerControlScript;
private int distancePlayer;
private int toggle;
private bool keepSpawning;
bool vehicleFlag = false;
bool plankFlag = false;
public float randomNumSpawn;
void Awake()
{
keepSpawning = true;
playerControlScript = GameObject.Find("PlayerObject").GetComponent<PlayerControl2>();
InvokeRepeating("Spawner", 3f, randomNumSpawn);
}
void Update()
{
if (Input.GetButtonDown("up") && !playerControlScript.gameOver)
SpawnField();
}
void Spawner()
{
bool activeLeft = false;
bool activeRight = false;
if (vehicleFlag)
{
print(initialObjectSpawn);
for (int i = 0; i < SpawnObjectVehicles.Length; i++)
{
print($"{SpawnObjectVehicles[i]}: {SpawnObjectVehicles[i].transform.position}"); //Here I get the weird position.z values pretty wonky
toggle = Random.Range(0, 2);
if (toggle == 1 && !activeLeft)
{
activeLeft = true;
SpawnObjectVehicles[i].SetActive(true);
}
if (toggle == 0 && !activeRight)
{
activeRight = true;
SpawnObjectVehicles[i].SetActive(true);
}
else
SpawnObjectVehicles[i].SetActive(false);
}
}
}
void SpawnField()
{
//I want to spawn the vehicles, planks, and trees in sets accordingly to the field (grass, river, road)
//For vehicles and planks, they can move horizontally from either -z or z boundaries
//NOTE: keepSpawning may be useless if i have a playerControlScript.gameOver already in here
if (keepSpawning)
{
distancePlayer += 3;
Vector3 intPos = new Vector3(0, 0, 0);
int i = Random.Range(0, 1000);
for (int j = 0; j < Field.Length; j++)
{
if (i >= Field[j].minProbabilityRange && i <= Field[j].maxProbabilityRange)
{
intPos = new Vector3(distancePlayer, -1f, 0);
GameObject Surface = Instantiate(Field[j].spawnField);
if (Surface.CompareTag("Grass"))
TreeToggle();
if (Surface.CompareTag("Road"))
{
vehicleFlag = true;
VehicleToggle();
}
// if (Surface.CompareTag("River")) this will be the same as vehicle
// {
// plankFlag = true;
// PlankToggle();
// }
//Add spawn for vehicles and planks with given spawnrate/spawn intervals
Surface.transform.position = intPos;
vehicleFlag = false;
plankFlag = false;
}
}
}
}
void TreeToggle()
{
int counter = 0;
for (int i = 0; i < SpawnObjectTrees.Length; i++)
{
int toggle = Random.Range(0, 2); //[0, 2)
if (toggle == 1 && counter < 5) //True and when there are already 5-4 trees to toggle
{
counter++;
SpawnObjectTrees[i].SetActive(true);
}
else //fills the rest to inactive Trees
SpawnObjectTrees[i].SetActive(false);
}
}
void VehicleToggle()
{
// I have Left and Right with 2 vehicles in each. My goal is to setActive one of them each side at a time with a different interval spawnrate and speed
Spawner();
}
void PlankToggle()
{
Spawner();
}
}
[System.Serializable]
public class Spawn
{
public GameObject spawnField;
public float minProbabilityRange = 0.0f;
public float maxProbabilityRange = 0.0f;
}
Hierarchy/Inspector
If there is any information you want to know, feel free to ask and I will make a quick edit to fulfill these goals. Again, thank you for your time and appreciate it :D I hope you are having a good day!
I've tried to make a script to spawn objects at a random position without them colliding with one another. It doesn't work properly as the OverlapBox returns almost every time null even when it touches a square.
Here is the script:
var quadBoundaries = quad.GetComponent<MeshCollider>().bounds;
var squareRadius = new Vector2(1, 1);
foreach (var square in squaresToSpawn)
{
_isOverlapping = true;
while (_isOverlapping)
{
_spawnPoint = new Vector2(Random.Range(quadBoundaries.min.x + 1.5f, quadBoundaries.max.x - 1.5f),
Random.Range(quadBoundaries.min.y + 1.5f, quadBoundaries.max.y - 1.5f));
_collisionWithSquare = Physics2D.OverlapBox(_spawnPoint, squareRadius,
0, LayerMask.GetMask("Square Layer"));
if (_collisionWithSquare is null)
{
square.transform.position = _spawnPoint;
_isOverlapping = false;
}
}
}
The quadBoundaries are the boundaries of a quad I placed so the squares will randomly spawn in a limited space.
My understanding is that I am generating a random point in the quad boundaries and then I check if on that point a square of scale (1,1) will fit without touching any other thing that has a collider and is on the square layer. if it touches then I generate a new point until the collision is null so I can place the square at the designated position.
But a bunch of things that I don't understand are happening.
First, the squares are touching each other. Second, just a few specific squares are registering a collision but even those are being touched by other squares. Third, when I scale up the square radius (for example 10,10) I get a big split between the squares (shown in the picture bellow).
I must add that all squares have a collider, are all on the square layer and the quad is on a different layer.
Can anybody explain to me what I'm not getting here? Thanks a lot!
Before the answer I'd like to say, that such algorithm of spawning is very dangerous, because you can enteran infinity loop, when there will no be place for new square. Minimum, that you can do to make this code more safe is to add a retries count for finding a place to spawn. But I will leave it on your conscience.
To make this algorithm work, you should understand that all physics in Unity are updated in fixed update. So all operations, that you do with Phisycs or Phisics2D are working with the state of objects, that was performed in the last Pyhsics update. When you change the position of the object, physics core don't capture this changes instantly. As a workaround you can spawn each object in the fixed update separately. Like this:
public class Spawner : MonoBehaviour
{
[SerializeField] private GameObject[] _squaresToSpawn;
[SerializeField] private GameObject _quad;
// Start is called before the first frame update
void Start()
{
StartCoroutine(Spawn());
}
private IEnumerator Spawn()
{
var quadBoundaries = _quad.GetComponent<MeshCollider>().bounds;
var squareRadius = new Vector2(1, 1);
Vector2 spawnPoint;
foreach (var square in _squaresToSpawn)
{
yield return new WaitForFixedUpdate();
var isOverlapping = true;
var retriesCount = 10;
while (isOverlapping && retriesCount > 0)
{
spawnPoint = new Vector2(Random.Range(quadBoundaries.min.x + 1.5f, quadBoundaries.max.x - 1.5f),
Random.Range(quadBoundaries.min.y + 1.5f, quadBoundaries.max.y - 1.5f));
var hit = Physics2D.OverlapBox(spawnPoint, squareRadius,
0, LayerMask.GetMask("Square"));
if (hit is null)
{
square.transform.position = spawnPoint;
isOverlapping = false;
}
else
{
retriesCount--;
if (retriesCount == 0)
{
Debug.LogError("Can't find place to spawn the object!!");
}
}
}
}
}
}
But such code will have effect of continious spawning:
To make objects spawning properly within one frame. You should manualy store all spawned objects bounding boxes inside your code, and manualy check if your new spawn bounding box collide with previously spawned objects.
Please look at example - http://www.mathplayground.com/mancala.html
Can anyone suggest the logic to :
1) spawn objects at positions
2) Pick up all objects on click and distribute them one by one.
3) Is it better to create all objects or instantiate them on the fly. ?
I tried code below but it just instantiates all objects at once.
if (HoleHandler.gemCount_Hole1_Update_Flag == true)
{
foreach (GameObject g in gemList1)
{
Destroy(g);
//want to add a time delay of 2 secs here
}
if (gemCount_Hole1 > 0)
{
for (int i = 0; i < gemCount_Hole1; i++)
{
int Gem_prefabIndex = UnityEngine.Random.Range(0, 9);
gemList1.Add(Instantiate(Gem_prefabList[Gem_prefabIndex], new Vector2((xPos_Hole1 + (Random.Range(-20, 20))) * 2.0F, (-229 + (20 * i))), Quaternion.identity));
}
}
}
I'm not 100% sure of what you're trying to achieve but I will answer as best I can.
For a start, any gameobject you are going to be instantiating (spawning) at run time should ideally be done so from a prefab.
Secondly, to spawn them at random intervals you want to be checking if they should be spawned at different time frames. This can be achieved through a co-routine or the Update function. I would recommend Update if this is new to you.
Update is called every frame.. and it's with this that you can achieve timed events. You can use a variety of helper methods to determine the time since the last frame or the real time elapsed.
For example
public class MyGameObject : Monobehaviour {
void Start() {
//This is called first, use it to set up whatever you want.
}
void Update() {
//This will be called every frame.
//Each frame or time lapse will determine if I should spawn
// a new gameobject.
}
}
Update
After looking at the game you have linked in your post I can offer the following advice.
Something like the following may point you in the right direction.
public int[] gemsInCups = new int [] {4,4,4,4,4,4,0,4,4,4,4,4,4,0};
public void Distribute(int position){
int gems = gemsInCups[position];
for(int i = position + 1; gems > 0; i++){
gemsInCups[position] ++;
gems --;
//Check the end of the array has not been reached.
//If it has, start distributing again from the first position provided
// there are still gems to distribute.
}
}
You will need some additional logic to finish this.
What you should remember is, I usually find it much more manageable keeping my data and my view (gameobjects) under different scopes... but the view will change to reflect the data and does not directly alter it. Now you know how many gems there are in each cup, you can simply update this each frame.
[UPDATE] I tried using the suggestion below but am still having an issue where the enemies appear to move simultaneously and they each move twice when first seen if there is already an enemy in line of sight. All I really want to do is have a delay between the actions of enemies in the player s line of sight so they do not simultaneously move, play sounds etc...
The updated code is:
private IEnumerator takeAllEnemyActions()
{
// Cycle through all enemies and have them perform their actions
if (Enemies != null && Enemies.Length > 0)
{
bool foundEnemyInLoS = false;
Enemy lastEnemy = null;
foreach (GameObject enemy in Enemies)
{
// Cache the enemies script
Enemy thisEnemy = enemy.GetComponent<Enemy>();
// If it is not this enemies turn to move then move on to the next enemy
if (thisEnemy.NextMoveTick > GameManager.Instance.CurrentTick)
{
continue;
}
// Check to see is this enemy is the first enemy in the players line of sight (If there is one)
if (!foundEnemyInLoS && thisEnemy.isInLineOfSightOfPlayer())
{
// If so then we save it for last to ensure that if any enemy is called with a delay that the last one does not make the player wait to move
foundEnemyInLoS = true;
lastEnemy = thisEnemy;
continue;
}
// At this point only enemies whose turn it is and who are not being saved for later can act
thisEnemy.TakeEnemyActions();
// If the enemy was in the players line of sight wait 200ms before continuing
if (thisEnemy.isInLineOfSightOfPlayer())
{
yield return new WaitForSeconds(0.5f);
}
}
// if there were enemies in the players line of sight then one was saved for last
// Take its action without a delay so the player can go next
if (foundEnemyInLoS)
{
lastEnemy.TakeEnemyActions();
}
}
Original Question:
In a top down, 2d, turn based roguelike in Unity3d 5 I am having an issue with game pacing. I am using a very simple state machine to control execution but I want enemies that all move in the same turn to do so with a pause between each one if they are in the players line of sight so that animations and sound have time to occur without overlapping (Especially sounds). The only issue I am having is getting the actual pause to occur without affecting animations or camera movement (The camera may be sliding to follow a player movement and it was stopping). I only need about a 100ms pause before moving to the next enemy.
The enemies are taking their actions in a foreach loop in an EnemyManager script
private void takeAllEnemyActions()
{
// Cycle through all enemies and have them perform their actions
if (Enemies != null && Enemies.Length > 0)
{
foreach (GameObject enemy in Enemies)
if (enemy.GetComponent<Enemy>().NextMoveTick <= GameManager.Instance.CurrentTick)
enemy.GetComponent<Enemy>().TakeEnemyActions();
}
GameManager.States.NextPlayState();
}
I tried creating a routine to invoke each enemy to act but the issue there was that execution of other enemies continued.
I tried using a coroutine but again the issue was the game would move on into the next state.
I even tried a while-do loop using Time.RealTimeSinceStartup just to see what it would do (I know, a very VERY bad idea)
I'm certain this is simple and I am just having a brain cramp but I've been trying things and google-binging for hours with no progress. Thank goddness for git to roll back.
Does anyone have any suggestions on the best way to go? I'm not needing someone to write my code, I just need pointed in the right direction.
Thanks
Use a co-routine and yield for each statement. Here's some modified code.
private IEnumerator TakeAllEnemyActions() {
// Cycle through all enemies and have them perform their actions
if (enemies != null && enemies.Length > 0) {
foreach (var enemy in enemies) {
if (enemy.GetComponent<Enemy>().NextMoveTick <= GameManager.Instance.CurrentTick) {
enemy.GetComponent<Enemy>().TakeEnemyActions();
yield return new WaitForSeconds (0.1f);
}
}
}
GameManager.States.NextPlayState();
}
Call this method with
StartCoroutine(TakeAllEnemyActions());
This has me utterly confused. I am using unity3d and c# for the scripts and it seems as if the code is running twice per frame. However on button down I have a sprite change position and it only changes once at least I think it does.
I added the Debug in and I am getting results like this:
score 1 at 3.569991 at frame 168
score 2 at 3.57414 at frame 168
score 3 at 3.818392 at frame 183
score 4 at 3.820178 at frame 183
and so forth carrying on. I am not updating the score in any other scripts. There is more to this script but it is just printing the score out on screen.
Is there any reason why a script may run like this?
full script:
using UnityEngine;
using System.Collections;
public class Score : MonoBehaviour {
public static int highScore;
public static int myScore;
public static bool allowScore;
public GUIText myText;
public static bool WhichScene;
//only allows score to start when first object has passed player object
void OnTriggerEnter2D(Collider2D collisionObject) {
allowScore = true;
Debug.Log ("allowScore is true");
}
void Start () {
highScore = PlayerPrefs.GetInt("highScore");
int scale = Screen.height / 20;
myText.fontSize = scale;
}
//add 1 to score every switch
void Update () {
// need to stop score counting
if (DeathOnImpact.dead == true) {
allowScore = false;
} if (Input.GetMouseButtonDown (0) && allowScore == true && WhichScene == true) { // added SpawnerObjectMovement.WhichScene == true
//Input.GetMouseButtonDown (0)
//Input.GetKeyDown("space")
myScore = myScore + 1;
Debug.Log ("My score is " + myScore + " point(s)" + " at time:" + Time.realtimeSinceStartup + " at frame:" + Time.frameCount);
} if (myScore > highScore) {
highScore = myScore;
PlayerPrefs.SetInt("highScore", highScore);
}
myText.text = myScore.ToString ();
//myText.text = "Score: " + myScore.ToString ();
if (Score.WhichScene == false) {
int scale = Screen.height / 40;
myText.fontSize = scale;
myText.text = "practice mode";
}
}
}
The script is attached to a TriggerObject, a sprite, and a Gui Text
WhichScene referes to which button I pressed, 'play' for normal play or 'practice mode' for an easier version. Score is disabled for 'practice mode'.
UPDATE: I have just edited out everything that I have added since the problem arose and it has not been fixed. Im going to check all unity setting to see if anything has changed. It seems in build setting an old Scene which I deleted around when the problem arose is not selcted but just 'shadowed' so I cant select it. The Scene was an exact copy of the PlayScene. Is this a sign of the problem?
SOLUTION: It appears that separating the script into two smaller scripts has solved the issue. I am still unsure why it has only arisen now since it was working before as one, but oh well I guess. Thank you.
Based on your comments, where you have said that your script is attached to 3 GameObjects, that means that the Update() method is getting called 3 times per frame.
public class Score : MonoBehaviour {
public static int myScore;
You have declared myScore as a static int. This functionally means it will be shared by all instances of the Score script that run.
The Update() method of MonoBehaviour is called once per frame for every GameObject that has this script attached. You have 3 GameObjects with this script attached. Therefore, each will call Update() on their individual instance of the Score script.
I'm not sure what you exactly intend to happen, so it's hard for me to give any advice beyond pointing out the problem.
I think that you need to split this script into multiple scripts. This script is doing too much. It's violating the Single Responsibility Principal (SRP). This is one of the most important principles to follow in programming. I'd suggest splitting this into at least 3 scripts. I'd probably make the following scripts:
PlayerStatistics - Attach this script to your player object (I'm assuming that is the sprite you mentioned). It will simply hold the statistics, including score, for your player. There should only be one attached to each player.
ScoreBoard - Attach this script to your GUI component. It will take a reference to the PlayerStatistics. Notice this is a reference to the single instance that is on your Player. Your ScoreBoard script will only read the value of the score from the PlayerStatistics script.
ScoringTrigger - Attach this to your TriggerObject. It would also have a reference to the PlayerStatistics script. It would have the code that checks to see if scoring should be done, and updates the value of the PlayerStatistics script.
To avoid some events to run twice a frame use Events to get imput actions:
if(Event.current.type == EventType.MouseDown){
if(Event.current.button == 0 && allowScore && WhichScene) {
// do it once!
}
}