I have a List that tracks "alive" objects in runtime. And when I apply some behavior in items from this list I've found something strange (at least to me), for example, let's say that I want to damage each enemy in current scene, so (Items is a List of custom class)
if (Input.GetKeyDown(KeyCode.C))
{
var enemiesWithLife = enemiesInScene.Items.FindAll(enemy => enemy.life > 1);
foreach (var enemy in enemiesWithLife)
enemy.TakeDamage(1);
}
The above code works as expected BUT if I want to apply something to ALL enemies and try something as
if (Input.GetKeyDown(KeyCode.C))
{
foreach (var enemy in enemiesInScene.Items)
enemy.TakeDamage(1);
}
When I press "C" only 1 enemy "TakeDamage" and an error about list been modified during iterating
EnemiesInScene is an ScriptableObject, so the enemies list exist only in assets
BUT if I try same code, but make an find before, as
if (Input.GetKeyDown(KeyCode.C))
{
var allEnemies = enemiesInScene.Items.FindAll(enemy => true);
foreach (var enemy in allEnemies)
enemy.TakeDamage(1);
}
Things work as expected again ... WHY?
Edit
Codes asked in comments:
public void TakeDamage(int damage)
{
if (currentLife <= 0)
return;
currentLife -= damage;
if (currentLife <= 0)
Die();
else
TakeHit();
}
EnemiesInScene is an RuntimeSet
public class RuntimeSet : ScriptableObject
{
public List<RuntimeItem> Items = new List<RuntimeItem>();
public void Add(RuntimeItem thing)
{
if (!Items.Contains(thing))
Items.Add(thing);
}
public void Remove(RuntimeItem thing)
{
if (Items.Contains(thing))
Items.Remove(thing);
}
}
Runtime item
public class RuntimeItem : LazyComponents
{
public RuntimeSet runtimeSet;
private void OnEnable()
=> runtimeSet.Add(this);
private void OnDisable()
=> runtimeSet.Remove(this);
}
And LazyComponents is only a lazy approach wrapper that inherits from monobehaviour to get some components as rigidbody2d, etc...
foreach uses an enumerator, and enumerators can't change the underlying collection, but can, however, change any objects referenced by an object in the collection see MSDN
2 solutions: copy the collection in Array or List (less performance)
if (Input.GetKeyDown(KeyCode.C))
{
foreach (var enemy in enemiesInScene.Items.ToArray())
enemy.TakeDamage(1);
}
or use for loop which allows the modification in collection (more performance) i recommand
for(int i = 0 ; i < enemiesInScene.Items.Length; i++)
enemiesInScene.Items[i].TakeDamage(1);
I think that I've found my error, enumerator runs asynchronous, so it'll change while other things happen, so
When I try it
if (Input.GetKeyDown(KeyCode.C))
{
var allEnemies = enemiesInScene.Items.FindAll(enemy => true);
foreach (var enemy in allEnemies)
enemy.TakeDamage(1);
}
AllEnemies is already filled, so any change in enemiesInScene.Items don't affect allEnemies, this is why in other case we got errors
for (int i = list.Count - 1; i != -1; i--)
list.RemoveAt(i); // replace with useful code that can modify this list
Related
I tried to make a dynamic scroll view to display selectable options that can be both generated or destroyed during runtime but for some reason it only lets me to generate up to three, otherwise I get a Missing Reference Exception.
When I generate one then delete it, it will also give a Missing Reference Exception.
But when I generate two, then delete one or even two, everything works just fine no matter what I do.
How can I fix this strange Behaviour?
The code:
private List<JL> JLList;
public List<GameObject> JLID;
public GameObject ButtonPrefab;
public GameObject PrefabParent;
public GameObject JLMenuManager;
public GameObject ScrollViewContent;
private void Update()
{
JLList = JLMenuManager.GetComponent<AddJL>().JLList;
CheckForChange();
}
private void GenerateOptions()
{
foreach (Transform child in ScrollViewContent.transform)
{
Destroy(child.gameObject);
}
for (int i = 0; i < JLList.Count; i++)
{
GameObject newJLOption = Instantiate(ButtonPrefab, PrefabParent.transform);
JLID.Add(newJLOption);
newJLOption = JLID[i];
int JLIndex = i;
newJLOption.GetComponent<Button>().onClick.AddListener(() => LoadJLOptions(JLIndex));
}
}
private void LoadJLOptions(int JLIndex)
{
SendMessage("SetJL", JLIndex);
Debug.Log(JLIndex);
}
private void CheckForChange()
{
int allJLOptions = ScrollViewContent.transform.childCount;
if (allJLOptions != JLList.Count)
{
GenerateOptions();
}
}
It seems like it is only spawning in how many JLList.Count is set to, open your script JLList and there should be a variable called count . Check how many that is set to. If it is 3, then that is your issue. If not, then follow what #BugFinder said and try remove them from the list
Something that has been bothering me for a long time is why do the following lines of code have the same results.
Code 1:
Transform[] childs = gameObject.GetComponentsInChildren<Transform>();
foreach(Transform child in childs) { Debug.Log(child.name); }
Code 2:
foreach(Transform child in gameObject.transform) { Debug.Log(child.name); }
This is actuallly a pseudo-code, I didn't really test it but should be enough to explain.
My question is, what's happening on Code 2 ? Is gameObject.transform actually an array of Transform ? Why doesn't Code 2 print the name of the parent gameObject ?
Maybe this is something very simple and obvious I'm just overlooking but I can't make it out right now.
Transform implements the IEnumerable interface. This interface is what allows the use of the foreach keyword.
public partial class Transform : Component, IEnumerable
The IEnumerable interface requires implementation of the GetEnumerator() method. The enumerator is responsible for keeping track of the position in the underlying collection and indicating if there are more items to be iterated over.
This is implemented in Transform below
public IEnumerator GetEnumerator()
{
return new Transform.Enumerator(this);
}
private class Enumerator : IEnumerator
{
Transform outer;
int currentIndex = -1;
internal Enumerator(Transform outer)
{
this.outer = outer;
}
//*undocumented*
public object Current
{
get { return outer.GetChild(currentIndex); }
}
//*undocumented*
public bool MoveNext()
{
int childCount = outer.childCount;
return ++currentIndex < childCount;
}
//*undocumented*
public void Reset() { currentIndex = -1; }
}
I am learning something about Serialization in Unity, and know that ISerializationCallbackReceiver can be used to help serialize some complex data structure that cannot serialize directly. I have tested the following code in Unity 2017 and Find some problems.
public class SerializationCallbackScript : MonoBehaviour, ISerializationCallbackReceiver
{
public List<int> _keys = new List<int> { 3, 4, 5 };
public List<string> _values = new List<string> { "I", "Love", "Unity" };
//Unity doesn't know how to serialize a Dictionary
public Dictionary<int, string> _myDictionary = new Dictionary<int, string>();
public void OnBeforeSerialize()
{
_keys.Clear();
_values.Clear();
foreach (var kvp in _myDictionary)
{
_keys.Add(kvp.Key);
_values.Add(kvp.Value);
}
}
public void OnAfterDeserialize()
{
_myDictionary = new Dictionary<int, string>();
for (int i = 0; i != Math.Min(_keys.Count, _values.Count); i++)
_myDictionary.Add(_keys[i], _values[i]);
}
void OnGUI()
{
foreach (var kvp in _myDictionary)
GUILayout.Label("Key: " + kvp.Key + " value: " + kvp.Value);
}
}
Obviously, this example show me how to serialize a dictionary by changing it into a list and resuming when deserialize. However, when i run the codes pratically, I find _keys and _values both empty (keys.Count = 0) in inspector. Even worse, i cannot modify their values in the inspector. As a result, nothing appears when entering the play mode.
So I want to know what is the actual usage of ISerializationCallbackReceiver and the reason of what happened.
OnBeforeSerialize is called in the editor every time the inspector updates (basically always), and it calls clear on your keys and value lists
i changed this for me like that:
#if UNITY_EDITOR
using UnityEditor;
#endif
public class ....{
public void OnBeforeSerialize() {
#if UNITY_EDITOR
if(!EditorApplication.isPlaying
&& !EditorApplication.isUpdating
&& !EditorApplication.isCompiling) return;
#endif
...
so it does not clear on edit-time.
So I'm getting ready to create my first game, I just finished classes on the C# language so I apologize if I'm using stuff such as interfaces wrong and all that. However, for my question; I'm trying different things and seeing what works. I've created a coin, and a player. The coin works as it should, however sometimes when I collect it, it will give me twice the points it should. The coins value is 15, sometimes when I collect a coin it'll add 15 points, other times it will add 30. How do I prevent this from happening.
Here's my code:
Coin Controller Class:
public class CoinController : MonoBehaviour, IEconomy {
private int MoneyValue;
void Start () {
MoneyValue = 15;
}
void Update () {
}
void OnTriggerEnter(Collider col) {
if (col.CompareTag("Player")) {
Destroy(transform.gameObject);
Value();
}
}
public int Value() {
return EconomyController.Money += MoneyValue;
}
}
Economy Controller:
public class EconomyController : MonoBehaviour{
public static int Money;
void Start() {
Money = 0;
}
}
Economy Interface:
public interface IEconomy {
int Value();
}
I would like to point some things about your code:
A good practice when declaring variables is using lowerCamelCase:
thisIsLowerCamelCase
ThisIsNot
This is a variable name convention that is largely used in programming to differentiate Methods and Classes from variables.
Another thing I've noticed is that your "Money" variable is static and it is still being updated on your CoinController. I'd set this variable to be private int variable and use a setter to update it. With that in mind... Have you tried to use Debug.Log to check if the OnTriggerEnter is triggering twice before the object is destroyed?
Simply write:
Debug.Log ("This should only happen once!");
And play the game. If your console shows this message two times, this trigger is being called twice. Another thing that you might notice is that you are calling the Value () method after you called the Destroy (transform.gameObject).
I would do something like:
public class CoinController : MonoBehaviour{
private int moneyValue = 15;
private EconomyController economyController;
void Start (){
economyController = FindObjectOfType (typeof (EconomyController)) as EconomyController;
}
void OnTriggerEnter (Collider col) {
if (col.CompareTag("Player")) {
AddValue();
}
}
public int AddValue() {
EconomyController.money += moneyValue; //Option one.
EconomyController.AddMoney (moneyValue) ; //Option two.
DestroyGameObject ();
}
private void DestroyGameObject (){
Destroy(transform.gameObject);
}
}
Using the clean code principles, option 2 is using a public void function created inside the EconomyController class changing a private variable.
My intuition tells me that you are probably collecting two coins at the time. I'm not sure how you are setting out the coins but I've had a similar problem before.
Imagine a game of snake. Lets say you've programmed it so once you eat a square you create a new one to a random location. There is a probability that the new square would appear inside the snake so it would instantly be eaten. This could be why it happens only some of the time.
Try disabling the collider before you destroy it.
Destroying a gameobject isn't instant and it's (annoyingly) quite easy to set off triggers multiple times.
void OnTriggerEnter(Collider col) {
if (col.CompareTag("Player")) {
// Pseudo Code: GetComponent<TheColliderItIs>().Enabled = false;
Value();
Destroy(transform.gameObject);
}
}
I'm creating a game in Unity and I'm having some trouble with it. I have 3 different child objects within a parent object, i would like to randomly set 1 of these 3 child objects as the active object and simultaneously disable the other two. I would like this to happen on colliding with another object.
Thanks in advance.
public GameObject parentOfChild;
void OnTriggerEnter(Collider thing)
{
if("the collision condition")
{
int randomChild = Random.Range(0,2);
if(randomChild == 0)
{
parentOfChild.transform.GetChild(0).gameObject.SetActive(true);
parentOfChild.transform.GetChild(1).gameObject.SetActive(false);
parentOfChild.transform.GetChild(2).gameObject.SetActive(false);
}
else
if(randomChild == 1)
{
parentOfChild.transform.GetChild(0).gameObject.SetActive(false);
parentOfChild.transform.GetChild(1).gameObject.SetActive(true);
parentOfChild.transform.GetChild(2).gameObject.SetActive(false);
}
else
if(randomChild == 2)
{
parentOfChild.transform.GetChild(0).gameObject.SetActive(false);
parentOfChild.transform.GetChild(1).gameObject.SetActive(false);
parentOfChild.transform.GetChild(2).gameObject.SetActive(true);
}
}
}
This is considering that all three children are not visible until the collision.It will also work if all three children visible.
In the parentOfChild
object pass your gameobject having the 3 children
I recommend yo to do this instead
public GameObject parentOfChild;
void OnTriggerEnter(Collider thing)
{
int randomChild = Random.Range(0,2);
parentOfChild.transform.GetChild(0).gameObject.SetActive(false);
parentOfChild.transform.GetChild(1).gameObject.SetActive(false);
parentOfChild.transform.GetChild(2).gameObject.SetActive(false);
parentOfChild.transform.GetChild(randomChild).gameObject.SetActive(true);
}