I'm trying to make a custom object pooling class (this is my first time trying to avoid using Instantiate().
I know object pooling is objectively better than instantiating but I'm worried that the way I set it up might actually be worse than instantiating since I have a long List of gameobjects, and in order to find a reusable gameobject I have to use a foreach loop to loop through all the elements of the list.
using System.Collections.Generic;
using UnityEngine;
public class ObjectPooler : MonoBehaviour
{
[SerializeField] GameObject redHurtEffect;
[SerializeField] GameObject Arrow;
static List<GameObject> pooledObjects = new List<GameObject>(2);
private void Awake()
{
for (int i = 0; i < 15; i++)
{
GameObject theEffect = Instantiate(redHurtEffect);
pooledObjects.Add(theEffect);
theEffect.SetActive(false);
}
for (int i = 0; i < 550; i++)
{
GameObject theEffect = Instantiate(Arrow);
pooledObjects.Add(theEffect);
theEffect.SetActive(false);
}
}
public static void Spawn(GameObject prefab, Vector3 spawnWhere, Quaternion? rotation =null, int repetitions =1, Vector2? setScale = null)
{
if (repetitions == 1)
{
Debug.Log(prefab.name);
GameObject spawned = prefabToPooled(prefab);
spawned.SetActive(true);
spawned.transform.position = spawnWhere;
if(setScale!=null || setScale.HasValue)
{
spawned.transform.localScale = setScale.Value;
}
if (rotation.HasValue)
{
spawned.transform.rotation = rotation.Value;
}
}
else
{
for (int i = 0; i < repetitions+1; i++)
{
// convertedPrefab.SetActive(true);
// convertedPrefab.transform.position = spawnWhere;
}
}
}
public static GameObject Spawn(GameObject prefab, Vector3 spawnWhere, Quaternion? rotation = null, Vector2? setScale = null, Transform parent = null)
{
GameObject spawned = prefabToPooled(prefab);
Transform spawnedTransform = spawned.GetComponent<Transform>();
Debug.Log(prefab.name);
spawned.SetActive(true);
spawnedTransform.position = spawnWhere;
if (rotation.HasValue)
{
spawnedTransform.rotation = rotation.Value;
}
if (setScale != null || setScale.HasValue)
{
spawnedTransform.localScale = setScale.Value;
}
spawnedTransform.SetParent(parent);
return prefabToPooled(prefab);
}
public static void rePool(GameObject gameObj)
{
gameObj.transform.SetParent(null);
pooledObjects.Add(gameObj);
gameObj.SetActive(false);
}
public static GameObject prefabToPooled(GameObject prefab)
{
string prefabName = prefab.name;
foreach (var item in pooledObjects)
{
if(item.name.Contains(prefabName))
{
item.SetActive(true);
pooledObjects.Remove(item);
return item;
}
}
return null;
}
}
The performance seems the same? I tried fake instantiating arrows every frame using the object pooling, and then I tried normally instantiating, and unless I'm blind the results seemed to be the same.
Also I'm worried if it'll be slower since I just started using this, I have so much more items to add to the game. The pooledObjects List might contain thousands or even tens of thousands of elements (since I want to add treasure like "diamond crown, emerald crown, coin, necklace" and I'd like to have many of those per level)
Would foreach looping through those thousands of elements be worse than instantiating?
Thanks!
Related
I have a 3D board game in Unity. I would like to move my character without having to press a key, but most importantly I would like to show a dynamic panel in canvas for whatever square the character lands on. So far I have the dice rolling and the character moving (after pressing a key) the correct amount of squares, but I am unable to figure out how to activate the panel based on the square color. Any help would be appreciated.
Here is my CharacterScript:
public class CharacterScript : MonoBehaviour
{
public Path currentPath;
public int squarePosition;
public int squares;
bool isMoving;
public GameObject PinkSquarePanel = GameObject.FindGameObjectWithTag("PinkSquare");
public GameObject CyanSquarePanel = GameObject.FindGameObjectWithTag("CyanSquare");
public GameObject WhiteSquarePanel = GameObject.FindGameObjectWithTag("WhiteSquare");
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Escape) && !isMoving)
{
squares = DiceNumberTextScript.diceNumber;
}
StartCoroutine(Move());
if (squares == 0)
{
ActivateSquarePanel();
}
}
IEnumerator Move()
{
if (isMoving)
{
yield break;
}
isMoving = true;
while (squares > 0)
{
Vector3 nextPosition = currentPath.squareList[squarePosition + 1].position;
while (MoveToNextSquare(nextPosition))
{
yield return null;
}
yield return new WaitForSeconds(0.1f);
squares--;
squarePosition++;
}
isMoving = false;
}
bool MoveToNextSquare(Vector3 goal)
{
return goal != (transform.position = Vector3.MoveTowards(transform.position, goal, 4f * Time.deltaTime));
}
void ActivateSquarePanel()
{
if (squarePosition.Equals(PinkSquarePanel))
{
PinkSquarePanel.GetComponent<CanvasGroup>().alpha = 1;
}
else if (squarePosition.Equals(CyanSquarePanel))
{
CyanSquarePanel.GetComponent<CanvasGroup>().alpha = 1;
}
else if (squarePosition.Equals(WhiteSquarePanel))
{
WhiteSquarePanel.GetComponent<CanvasGroup>().alpha = 1;
}
}
}
And here is my PathScript:
public class Path : MonoBehaviour
{
Transform[] squareObjects;
public List<Transform> squareList = new List<Transform>();
GameObject[] PinkSquares = GameObject.FindGameObjectsWithTag("PinkSquare");
PinkSquare[] pinkList = new PinkSquare[1];
GameObject[] CyanSquares = GameObject.FindGameObjectsWithTag("CyanSquare");
CyanSquare[] cyanList = new CyanSquare[1];
GameObject[] WhiteSquares = GameObject.FindGameObjectsWithTag("WhiteSquare");
WhiteSquare[] whiteList = new WhiteSquare[1];
private void OnDrawGizmos()
{
Gizmos.color = Color.black;
FillSquares();
for (int i = 0; i < squareList.Count; i++)
{
Vector3 currentPosition = squareList[i].position;
if (i > 0)
{
Vector3 previousPosition = squareList[i - 1].position;
Gizmos.DrawLine(previousPosition, currentPosition);
if(currentPosition.Equals(PinkSquares))
{
pinkList[i] = new PinkSquare();
}
else if (currentPosition.Equals(CyanSquares))
{
cyanList[i] = new CyanSquare();
}
else if (currentPosition.Equals(WhiteSquares))
{
whiteList[i] = new WhiteSquare();
}
}
}
}
void FillSquares()
{
squareList.Clear();
squareObjects = GetComponentsInChildren<Transform>();
foreach (Transform square in squareObjects)
{
if (square != this.transform)
{
squareList.Add(square);
}
}
}
}
I believe your issue is in your comparisons, you are trying to use an Equals to compare your currentPosition which is a Vector3 type to a GameObject[] which is an array of gameObjects. As I mentioned in my comments, this comparison will always fail as an array of gameObjects can not be equal to a vector.
Instead of using these lines, try this line:
if(squareList[i].gameObject.tag.CompareTag("PinkSquare")
The full snippet of if else would look like
if(squareList[i].gameObject.tag.CompareTag("PinkSquare")
{
pinkList[i] = new PinkSquare();
}
else if(squareList[i].gameObject.tag.CompareTag("CyanSquare")
{
cyanList[i] = new CyanSquare();
}
else if(squareList[i].gameObject.tag.CompareTag("WhiteSquare")
{
whiteList[i] = new WhiteSquare();
}
Your CharacterScript is going to need to get the gameObject or Transform from the Path script as it is only keeping track of indexes. Your issue in this script is you are comparing an integer to a GameObject which would never be true. I would also recommend not using OnDrawGizmos() as it is an editor only script and should only be used to render editor debugging tools. The only reference to a Gizmo I see in the function is Gizmos.color = Color.black; which does nothing as you are not rendering a gizmo anywhere. I would move this code to a different function and call it from your CharacterScript. Have the return type be GameObject or Transform of the square you are on, so the CharacterSCript can check which color it lands on. Using an Integer nor Vector3 to compare to a GameObject[] will never work.
I am not sure if there are issues elsewhere in the code, but as this comparison would always fail, none of these statements would get broken into. What this means is your panels would never have the chance to get their alpha set nor get created.
I have a problem with the correct detection of object proximities using distance calculations and hope one of you can help me.
What i want:
There are several instantiated game objects with the same tag in my scene and I want to change their material color if their distance on the x and z axis is below "1". For this I iterate over a list of all objects and compare their position with the position of the current game object.
The problem:
Material colors are changing randomly on colliding objects and sometimes don't change back once the collision is over.
My code so far:
public class DistanceTester : MonoBehaviour
{
void Start()
{
}
void Update()
{
var menschen = GameObject.FindGameObjectsWithTag("Mensch");
float positionX = transform.position.x;
float positionZ = transform.position.z;
foreach (GameObject mensch in menschen)
{
float distanceX = Mathf.Abs(mensch.transform.position.x - positionX);
float distanceZ = Mathf.Abs(mensch.transform.position.z - positionZ);
if (gameObject != mensch) //Making sure the object is not the same
{
if (distanceX <= 1 && distanceZ <= 1)
{
GetComponent<Renderer>().material.color = Color.red;
mensch.GetComponent<Renderer>().material.color = Color.red;
}
else
{
GetComponent<Renderer>().material.color = Color.green;
mensch.GetComponent<Renderer>().material.color = Color.green;
}
}
}
}
}
I already tried to use triggers for collision detection but would like to use a more efficient way like in my example above.
The main issue is probbaly that you also set
GetComponent<Renderer>().material.color = ...;
so what if you are close to menschen[0] but far away from menschen[1] ?
→ You reset your color always with the result of the last item in menschen!
It sounds like you should rather only handle your own object since all of the other objects do the same thing right?
using Sytsem.Linq;
public class DistanceTester : MonoBehaviour
{
// reference this via the Inspector already
[SerializeField] private Renderer _renderer;
private void Awake()
{
// As fallback get it ONCE
if(!_renderer) _renderer = GetComponent<Renderer>();
}
private void Update()
{
// If possible you should also store this ONCE
var menschen = GameObject.FindGameObjectsWithTag("Mensch");
// This checks if ANY of the objects that is not this object is clsoe enough
if(menschen.Where(m => m != gameObject).Any(m => (transform.position - m.transform.position).sqrMagnitude < 1))
{
_renderer.material.color = Color.red;
}
else
{
_renderer.material.color = Color.green;
}
}
}
Where this Linq expression using Where and Any
menschen.Where(m => m!= gameObject).Any(m => (transform.position - m.transform.position).sqrMagnitude < 1)
Basically equals doing something like
var isClose = false;
foreach(var m in menschen)
{
if(m == gameObject) continue;
if((transform.position - m.transform.position).sqrMagnitude < 1)
{
isClose = true;
break;
}
}
if(isClose)
{
...
Note that it would still be more efficient if you can store the result of FindGameObjectsWithTag once instead of obtaining it every frame.
Assuming that any of your Mensch objects will have the component DistanceTester you could even implement some sort of "auto-detection" by using a pattern like
public class DistanceTester : MonoBehaviour
{
private static HashSet<DistanceTester> _instances = new HashSet<DistanceTester>();
private void Awake()
{
_instances.Add(this);
...
}
private void OnDestroy()
{
_instances.Remove(this);
}
...
}
Then you could quite efficient iterate through the _instances instead.
Even more efficient though would actually be to iterate only once from a global controller instead of doing that in each and every instance of DistanceTester!
I'm experimenting with the CullingGroup Api, however i'm unable to achieve any success because it looks like that my onStateChanged callback will not get called.
I have 24 spheres and a cube which has the following script attached to.
using UnityEngine;
public class CullingGroupBehaviour : MonoBehaviour
{
CullingGroup localCullingGroup;
public Transform[] Spheres;
public BoundingSphere[] cullingPoints;
void Awake()
{
localCullingGroup = new CullingGroup();
GameObject[] spheres = GameObject.FindGameObjectsWithTag("Spheres");
cullingPoints = new BoundingSphere[spheres.Length];
Spheres = new Transform[spheres.Length];
for (var i = 0; i < spheres.Length; i++)
{
Spheres[i] = spheres[i].transform;
cullingPoints[i].position = Spheres[i].position;
cullingPoints[i].radius = 4.0f;
}
localCullingGroup.onStateChanged = (CullingGroupEvent evt) => Debug.Log("Changed");
localCullingGroup.SetBoundingSpheres(cullingPoints);
localCullingGroup.SetBoundingSphereCount(cullingPoints.Length);
localCullingGroup.SetBoundingDistances(new float[] { 10.0f, 50.0f });
localCullingGroup.SetDistanceReferencePoint(transform.position);
}
void FixedUpdate()
{
localCullingGroup.SetDistanceReferencePoint(transform.position);
for (var i = 0; i < Spheres.Length; i++)
{
cullingPoints[i].position = Spheres[i].position;
}
}
void OnDestroy()
{
localCullingGroup.Dispose();
localCullingGroup = null;
}
}
The expected behaviour is that when i'm moving the cube the distances should change and the lambda expression should be called yet nothing happens.
Any ideas are appreciated!
Update:
The strange thing is that visibility events are sent correctly when using a camera but distance events does not trigger
Looks like you must set a camera in order to calculate distances, which is strange because the DOC says
If targetCamera is assigned then the bounding spheres will only be
culled from the perspective of that camera.
and
To have the CullingGroup perform visibility calculations, specify the
camera it should use
Yet nothing happens when you dont set the camera.
Additional Note:
The onStateChnaged event will only trigger when a BoundingDistance was passed eg: from 1 you enter into 2 (The example only contains one)
Working Solution:
using UnityEngine;
public class CullingGroupBehaviour : MonoBehaviour
{
private CullingGroup cullingGroup;
private BoundingSphere[] bounds;
Transform[] targets;
public Transform ReferencePoint;
void Start()
{
// All the objects that have a sphere tag
var gobjs = GameObject.FindGameObjectsWithTag("Sphere");
targets = new Transform[gobjs.Length];
for(int i = 0; i < gobjs.Length; i++)
{
targets[i] = gobjs[i].transform;
}
cullingGroup = new CullingGroup();
cullingGroup.targetCamera = Camera.main;
// Will automatically track the transform
cullingGroup.SetDistanceReferencePoint(transform);
// The distance points when the event will trigger
cullingGroup.SetBoundingDistances(new float[] { 25.0f });
// Creating Boundingspheres
bounds = new BoundingSphere[targets.Length];
for (int i = 0; i < bounds.Length; i++)
{
bounds[i].radius = 1.5f;
}
// Assigning the Bounding spheres
cullingGroup.SetBoundingSpheres(bounds);
// if not set it will use all of the array elements(so below code is redundant)
cullingGroup.SetBoundingSphereCount(targets.Length);
// Assigning an event when the distance changes
cullingGroup.onStateChanged = OnChange;
}
void Update()
{
for (int i = 0; i < bounds.Length; i++)
{
bounds[i].position = targets[i].position;
}
}
void OnDestroy()
{
cullingGroup.Dispose();
cullingGroup = null;
}
void OnChange(CullingGroupEvent ev)
{
if (ev.currentDistance > 0)
{
targets[ev.index].gameObject.GetComponent<Renderer>().material.color = Color.green;
}
else
{
targets[ev.index].gameObject.GetComponent<Renderer>().material.color = Color.red;
}
}
}
Hi I am working on game that uses random terrain and I want to spawn objects onto that terrain. To do this, I have created what I have called the Surface Populator Script.
This is the script:
public SurfaceSpawnerData spawnerData;
private float randomX;
private float randomZ;
private Renderer r;
void Start()
{
r = GetComponent<Renderer>();
for (int i = 0; i < spawnerData.spawnableObjects.Length; i++)
{
spawnerData.spawnableObjects[i].currentObjects = 0;
}
spawnerData.SpawnedObjects.Clear();
SpawnObjects();
}
void Update()
{
}
void SpawnObjects()
{
RaycastHit hit;
for (int i = 0; i < spawnerData.spawnableObjects.Length; i++)
{
int currentObjects = spawnerData.spawnableObjects[i].currentObjects;
int numOfObjects = spawnerData.spawnableObjects[i].numberOfObjects;
if (currentObjects != numOfObjects)
{
if (Physics.Raycast(new Vector3(randomX, r.bounds.max.y + 5f, randomZ), -Vector3.up, out hit))
{
randomX = Random.Range(r.bounds.min.x, r.bounds.max.x);
randomZ = Random.Range(r.bounds.min.z, r.bounds.max.z);
if (hit.point.y >= spawnerData.spawnableObjects[i].spawnerStartHeight && hit.point.y <= spawnerData.spawnableObjects[i].spawnerEndHeight)
{
spawnerData.SpawnedObjects.Add(Instantiate(spawnerData.spawnableObjects[i].spawnablePrefab, hit.point, Quaternion.identity));
spawnerData.spawnableObjects[i].currentObjects += 1;
}
}
}
}
}
The script also gains its data from a scriptable object:
[CreateAssetMenu]
public class SurfaceSpawnerData : ScriptableObject
{
public SpawnableObjects[] spawnableObjects;
public List<GameObject> SpawnedObjects;
[System.Serializable]
public class SpawnableObjects
{
public GameObject spawnablePrefab;
public float spawnerStartHeight = 2f;
public float spawnerEndHeight;
public int currentObjects;
public int numberOfObjects;
}
}
This script currently works perfectly fine when placed inside the update method, however I do not want to do this due to its affect on performance. Therefore I am wondering if there is a way to stop the Unity start method from exiting until my SpawnObjects() function has stopped running. If this is not possible if you have any other ideas on how I run this only once without using the update function let me know.
I am relatively new to c# as a language and I'm sorry if there is an easy fix that I have missed. Any help would be appreciated. Thanks.
Since SpawnObjects is a synchronus method Start will not return until SpawnObjects finished anyway.
As far as I understand your issue is rather that anything from Physics is not available during initialization (Awake, OnEnable, Start) but only within or after the Physics block (see ExecutionOrder) so e.g. in a method like FixedUpdate or Update.
So to answer your question: You could use a Coroutine and WaitForFixedUpdate in order to make your Instantiation:
void Start()
{
r = GetComponent<Renderer>();
for (int i = 0; i < spawnerData.spawnableObjects.Length; i++)
{
spawnerData.spawnableObjects[i].currentObjects = 0;
}
spawnerData.SpawnedObjects.Clear();
StartCoroutine(DoInstantiate());
}
private IEnumerator DoInstantiate()
{
// wait until Physics are initialized
yield return new WaitForFixedUpdate();
SpawnObjects();
}
or as you can see in ScriptReference/Coroutine you can make this shorter by directly making the Start a routine e.g. like
IEnumerator Start()
{
r = GetComponent<Renderer>();
for (int i = 0; i < spawnerData.spawnableObjects.Length; i++)
{
spawnerData.spawnableObjects[i].currentObjects = 0;
}
spawnerData.SpawnedObjects.Clear();
// wait until Physics are initialized
yield return new WaitForFixedUpdate();
SpawnObjects();
}
I'm trying to get variable from an empty gameObject's script, but I can't assign that gameObject on the inspector. These are the screen shots and codes from my game.
Well, I have this code to load when the game is starting. Land and Prince are objects that made from this code.
using UnityEngine;
using System.Collections;
using System;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
public class loadGame : MonoBehaviour
{
public static loadGame loadSave;
public GameObject objPrince;
public Pangeran charPrince;
public Transform prefPrince;
public Sprite[] spriteTanah;
public Dictionary<string, Tanah> myTanah = new Dictionary<string, Tanah>();
public Dictionary<string, GameObject>objTanah = new Dictionary<string, GameObject>();
public Tanah tempTanah;
public GameObject tempObjTanah;
public Transform prefTanah;
public float mapX;
public float mapY;
public int i = 0;
public int j = 0;
public int rows = 9;
public int column = 9;
void Awake(){
if(loadSave == null)
{
DontDestroyOnLoad(gameObject);
loadSave = this;
}
else if(loadSave != this)
Destroy(gameObject);
}
// Use this for initialization
void Start ()
{
Load ();
}
// Update is called once per frame
void Update ()
{
}
public void Load()
{
if(File.Exists(Application.persistentDataPath + "/playerInfo.dat"))
{
}
else
{
charPrince = new Pangeran ("Prince", "04Okt1993", 0, 0, 0, 0, 0, false, false);
objPrince = GameObject.Instantiate (prefPrince, new Vector3 (0, 0, 0), Quaternion.identity) as GameObject;
//objPrince.name = "Prince";
charPrince.locationY = 0f;
charPrince.locationX = 0f;
charPrince.hadapAtas = false;
charPrince.hadapKanan = true;
charPrince.stamina = 100f;
charPrince.exp = 0f;
charPrince.speed = 0f;
for(i = 0 ; i < rows ; i ++)
{
for(j = 0; j<column ; j++)
{
mapX = (i-j) * 0.8f;
mapY = (i+j) * 0.4f;
if(i>=1 && j>=1 && i<=5 && j<=5)
{
prefTanah.name = "land-"+j.ToString("0#")+"-"+i.ToString("0#");
tempTanah = new Tanah("land-"+j.ToString("0#")+"-"+i.ToString("0#"),mapX,mapY,"land",spriteTanah[0],spriteTanah[1],spriteTanah[2]);
myTanah.Add("land-"+j.ToString("0#")+"-"+i.ToString("0#"),tempTanah);
tempObjTanah = GameObject.Instantiate(prefTanah, new Vector3(mapX,mapY,0),Quaternion.identity)as GameObject;
objTanah.Add("land-"+j.ToString("0#")+"-"+i.ToString("0#"),tempObjTanah);
}
else
{
prefTanah.name = "snow-"+j.ToString("0#")+"-"+i.ToString("0#");
tempTanah = new Tanah("snow-"+j.ToString("0#")+"-"+i.ToString("0#"),mapX,mapY,"snow");
myTanah.Add("snow-"+j.ToString("0#")+"-"+i.ToString("0#"),tempTanah);
tempObjTanah = GameObject.Instantiate(prefTanah, new Vector3(mapX,mapY,0),Quaternion.identity)as GameObject;
objTanah.Add("snow-"+j.ToString("0#")+"-"+i.ToString("0#"),tempObjTanah);
}
}
}
}
}
}
I'm trying to access one of some variables from code above, but I can't assign it in the inspector.
but I can't do it.
please help me. Thank you.
The problem is that the loadLand variable is of type LoadGame which is a script. What you are trying to do is to add a GameObject to this variable. So change the public variable type to
public GameObject LoadLandObject;
private LoadGame loadLand;
and create a private LoadGame variable which is the reference to your script.
Add in the Start() method
loadLand = (LoadGame)LoadLandObject.GetComponent<LoadGame>();
With this you load the script LoadGame of the GameObject into the variable.
Do you ever set your plantingScript.loadLand to the instance of loadGame.loadSave? This must be done after Awake in your case to be sure the instance has been set.
Can I ask, what are you trying to do?
You should simply just assign you loadGame script in the inspector of plantingScript and not use statics at all. They will bite your ass sooner or later (and I'm guessing it already is).
Have a similar problem. Did n't find simple and clear solution. May be what i did help to you.
Create empty game object.
Attach loadGame.cs to it (if you whant to control when script starts - uncheck it,in this case don't foget to set loadLand.enabled to true in plantingScript when needed)
Drag this object to you loadLand field
Sample project