I'm trying to make an array or a list with the first childs of a GameObject so I can edit them from another scripts.
I have tried it doing an Array, but it will take the childs of this childs too and I need to take only the first ones.
I have tried also with a List with a foreach, but it will add the same element every frame to the list, and I only need it one time.
public GameObject GOGranjaTerrain;
public GameObject GOFabricaTerrain;
public GameObject GOOficinaTerrain;
public Transform[] granjaTerrainArray;
public Transform[] fabricaTerrainArray;
public Transform[] oficinaTerrainArray;
public List<Transform> granjaTerrainList = new List<Transform>();
void Update()
{
SearchTerrains1();
SearchTerrains2();
}
//Array way
void SearchTerrains1()
{
granjaTerrainArray = GOGranjaTerrain.GetComponentsInChildren<Transform>();
fabricaTerrainArray = GOFabricaTerrain.GetComponentsInChildren<Transform>();
oficinaTerrainArray = GOOficinaTerrain.GetComponentsInChildren<Transform>();
}
//List way
void SearchTerrains2()
{
foreach(Transform child in GOGranjaTerrain.transform)
{
granjaTerrainList.Add(child);
}
}
Here's how you can do it:
void Update()
{
for (int i = 0; i < GOGranjaTerrain.transform.childCount; i++)
{
Transform child = GOGranjaTerrain.transform.GetChild(i);
if (granjaTerrainList.Contains(child) == false)
{
granjaTerrainList.Add(child);
}
}
}
Or even better, if you only need to do it once - do in in Start instead of Update.
Related
I am practicing with C # Lists in Unity and I have encountered a problem.
My test script, instantiates 5 prefabs which are added in a gameobject list. I then wrote a code that generates a random int and from that number moves the prefab instantiated with that index (indexof). Everything works correctly, but the method that moves and deletes the prefab is repeated for all the gameobjects in the scene with an index higher than the one chosen. I enclose the two scripts to better explain the problem. (I would need the unlist method to be done only once.
how can i solve this problem and remove one item from the list at a time? (one each time the button is pressed, not all as it is now. Thanks)
script:
NpcController: Added in each instantiated prefab
ListCOntroller: added in the scene.
public class ListCOntroller : MonoBehaviour
{
public GameObject cubePrefab;
private GameObject cubeInstance;
public static List<GameObject> cubeList = new List<GameObject> ();
public TextMeshProUGUI checkText;
public static event Action chooseNpc;
public static int randNpcValue;
int rand;
private void Start()
{
for(int i =0; i < 5; i++)
{
cubeInstance = Instantiate(cubePrefab, new Vector3(i, -2, 0), Quaternion.identity);
cubeList.Add(cubeInstance);
}
}
public void CheckListText()
{
checkText.text = "Npc in list: " + cubeList.Count.ToString();
}
public static void SelectRandomCube()
{
randNpcValue = Random.Range(0, cubeList.Count);
chooseNpc?.Invoke();
}
}
public class NpcController : MonoBehaviour
{
void Start()
{
ListCOntroller.chooseNpc += NpcEvent;
}
private void NpcEvent()
{
if (ListCOntroller.cubeList.IndexOf(gameObject) == ListCOntroller.randNpcValue)
{
transform.localPosition = new Vector3(transform.position.x, 2, 0);
DeleteFromList();
}
}
private void DeleteFromList()
{
ListCOntroller.cubeList.RemoveAt(ListCOntroller.randNpcValue);
Debug.Log($"Delete from list: {ListCOntroller.randNpcValue}");
}
}
the int random number generated in the attached images is: 2
Because events are executed one after another.
Let's say you have 3 NPCs: NPC0, NPC1, NPC2
Now the random number you choosen is 1, when NPC1's NpcEvent runs, ListCOntroller.cubeList.IndexOf(gameObject) is 1 which equals to the randNpcValue, and then NPC1 will be removed from the list.
Note that now the list has 2 items left: NPC0, NPC2. Next NPC2's NpcEvent runs in turn, at this time, ListCOntroller.cubeList.IndexOf(gameObject) is still 1 because the list has only 2 items, so NPC2 is also removed from the list.
A solution is you can change the randNpcValue to an invalid value when a NPC is removed.
if (ListCOntroller.cubeList.IndexOf(gameObject) == ListCOntroller.randNpcValue)
{
transform.localPosition = new Vector3(transform.position.x, 2, 0);
DeleteFromList();
ListCOntroller.randNpcValue = -2;
}
In addition to this answer in general I don't really see the purpose mixing the the logic into two different scripts.
You have one for storing a list and raising an event and the other one listens to the event and manipulates the stored list -> This doesn't sounds right.
You could as well simply do `why don't you pass a long the the according object into the event in the first place and rather do something like
public class ListCOntroller : MonoBehaviour
{
// Singleton instance
private static ListCOntroller _instance;
// Read-only getter
public static ListCOntroller Instance => _instance;
// pass in the target object reference instead of doing things index based
public event Action<GameObject> chooseNpc;
public GameObject cubePrefab;
// THIS is the list controller -> nobody else should be able to manipulate this list
private readonly List<GameObject> cubeList = new ();
public TextMeshProUGUI checkText;
private void Awake()
{
if(_instance && _instance != this)
{
Destroy(this);
return;
}
_instance = this;
}
private void Start()
{
for(int i =0; i < 5; i++)
{
cubeInstance = Instantiate(cubePrefab, new Vector3(i, -2, 0), Quaternion.identity);
cubeList.Add(cubeInstance);
}
}
public static void SelectRandomCube()
{
if(cubeList.Count == 0)
{
Debug.LogWarning("Trying to pick item from empty list");
return;
}
var randomIndex = Random.Range(0, cubeList.Count);
var randomItem = cubeList[randomIndex];
// remove from the list yourself instead of relying on others to work correctly
cubeList.RemoveAt(randomIndex);
// pass along the target object
chooseNpc?.Invoke(randomItem);
}
}
and then
public class NpcController : MonoBehaviour
{
void Start()
{
ListCOntroller.Instance.chooseNpc += NpcEvent;
}
private void NpcEvent(GameObject target)
{
// check if you are the target object
if (target == gameObject))
{
// adjust your position - you don't care about other NPCs or the existence of a list
transform.localPosition = new Vector3(transform.position.x, 2, 0);
}
}
}
I am making a script for a unity project to destroy an instantiated clone if it collides with another clone, but since the Game Object (the clone) is declared in the start method and I cannot put it at the top I need to figure out how to destroy the clone if it collides with something else.
This is the error I get if I put it on top:
A field initializer cannot reference the non-static field, method, or property
Code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class topbigspike : MonoBehaviour
{
public GameObject Flame;
// Start is called before the first frame update
void Start()
{
int a = 30;
int i = 0;
while (0 < a)
{
a--;
i++;
GameObject FlameClone = Instantiate(Flame);
FlameClone.transform.position = new Vector3(Random.Range(10, 2000), -3, 0);
}
}
void OnCollisionEnter2D(Collision2D col)
{
Destroy(FlameClone);
}
}
As the error message says, you cannot use Flame as field initializer. But you can still declare FlameClone as field (at the top, as you say) ) and initialize it in the Start method:
public class topbigspike : MonoBehaviour
{
public GameObject Flame;
public GameObject FlameClone; // <=== Declare here, as class field.
// Start is called before the first frame update
void Start()
{
int a = 30;
int i = 0;
while (0 < a)
{
a--;
i++;
FlameClone = Instantiate(Flame); // <=== Initialize here.
FlameClone.transform.position = new Vector3(Random.Range(10, 2000), -3, 0);
}
}
void OnCollisionEnter2D(Collision2D col)
{
Destroy(FlameClone);
}
}
As already mentioned you need to store it a field in order to access it from other methods.
However, seeing a while loop there instantiating multiple (30) instances a single field isn't enough anyway except you really want to only destroy the last created instance.
It should probably rather be e.g.
public class topbigspike : MonoBehaviour
{
public int amount = 30;
public GameObject Flame;
private GameObject[] flameInstances;
void Start()
{
flameInstances = new GameObject[amount];
for(var i = 0; i < amount; i++)
{
var flameInstance = Instantiate(Flame);
flameInsance.transform.position = new Vector3(Random.Range(10, 2000), -3, 0);
flameInstances[i] = flameInstance;
}
}
void OnCollisionEnter2D(Collision2D col)
{
foreach(var flameInstance in flameInstances)
{
Destroy(flameInstance);
}
}
}
I am trying to assemble together human from 3D model that have all limbs split. Like Head,Torso,Arms,Legs etc. And in runtime I would like to build a whole human with all those limbs assembled together. Here is what I do:
private void Start()
{
Debug.Log("Trying to build a Race!");
RaceModelSO firstRace = _races[0];
GameObject stiches = Instantiate(firstRace.GetRaceBodySlots()[0].mesh, new Vector3(773,0.83F,778), Quaternion.identity) as GameObject;
stiches.name = "HumanMale";
for (int i = 0; i < firstRace.GetRaceBodySlots().Length; i++)
{
if (i != 0)
{
GameObject bodyPart = Instantiate(firstRace.GetRaceBodySlots()[i].mesh) as GameObject;
_stitcher.Stitch(bodyPart, stiches);
}
}
}
Here is the actual Stitcher class that is assembling the limbs together.
public class Stitcher
{
public GameObject Stitch(GameObject sourceClothing, GameObject targetAvatar)
{
var boneCatalog = new TransformCatalog(targetAvatar.transform);
var skinnedMeshRenderers = sourceClothing.GetComponentsInChildren<SkinnedMeshRenderer>();
var targetClothing = AddChild(sourceClothing, targetAvatar.transform);
foreach (var sourceRenderer in skinnedMeshRenderers)
{
var targetRenderer = AddSkinnedMeshRenderer(sourceRenderer, targetClothing);
targetRenderer.bones = TranslateTransforms(sourceRenderer.bones, boneCatalog);
}
return targetClothing;
}
private GameObject AddChild(GameObject source, Transform parent)
{
source.transform.parent = parent;
foreach (Transform child in source.transform)
{
Object.Destroy(child.gameObject);
}
return source;
}
private SkinnedMeshRenderer AddSkinnedMeshRenderer(SkinnedMeshRenderer source, GameObject parent)
{
GameObject meshObject = new GameObject(source.name);
meshObject.transform.parent = parent.transform;
var target = meshObject.AddComponent<SkinnedMeshRenderer>();
target.sharedMesh = source.sharedMesh;
target.materials = source.materials;
return target;
}
private Transform[] TranslateTransforms(Transform[] sources, TransformCatalog transformCatalog)
{
var targets = new Transform[sources.Length];
for (var index = 0; index < sources.Length; index++)
targets[index] = DictionaryExtensions.Find(transformCatalog, sources[index].name);
return targets;
}
#region TransformCatalog
private class TransformCatalog : Dictionary<string, Transform>
{
#region Constructors
public TransformCatalog(Transform transform)
{
Catalog(transform);
}
#endregion
#region Catalog
private void Catalog(Transform transform)
{
if (ContainsKey(transform.name))
{
Remove(transform.name);
Add(transform.name, transform);
}
else
Add(transform.name, transform);
foreach (Transform child in transform)
Catalog(child);
}
#endregion
}
#endregion
#region DictionaryExtensions
private class DictionaryExtensions
{
public static TValue Find<TKey, TValue>(Dictionary<TKey, TValue> source, TKey key)
{
TValue value;
source.TryGetValue(key, out value);
return value;
}
}
#endregion
}
So let me explain the issue. In firstRace.GetRaceBodySlots() I have all the prefabs with all limbs of the body. I get all of them and I call _stitcher.Stitch() for each of them.
The problem is that the first limb is the one created before the :
for (int i = 0; i < firstRace.GetRaceBodySlots().Length; i++)
{
if (i != 0)
{
GameObject bodyPart = Instantiate(firstRace.GetRaceBodySlots()[i].mesh) as GameObject;
_stitcher.Stitch(bodyPart, stiches);
}
}
And that only the second limb gets attached properly all the others are there but not visible. Take a look:
So SOH_HM_1_Head is the first limb that was created before the foreach than is the second one SOH_HM_1_Body which is the first one inside the loop and only that one was created visible from all inside the foreach loop. All others are created but invisible. Take a look:
Do you have any idea why only the first limb from inside the foreach loop gets created visible? Any idea how can I solve this issue.
P.S.
Looks like the exact same issue was described here: Here
Can you suggest any other method rather than coroutine ?
For all people searching for a way to stitch many meshes into one all using same rig, I've come up with some small script: Unity-3D-MeshStitcher
I am at begginner level with unity.
I have Load() function that goes off in OnApplicationPause(false). It works fine if I block the screen or minimalise app, and come back to it. However, when I kill it, I get error and the data doesnt get loaded.
Below is the script attached to the GameObject "SaveManager"
using System.Collections.Generic;
using UnityEngine;
using System;
public class SaveManager : MonoBehaviour
{
public GameObject ZwierzetaGroup;
public GameObject JedzeniaGroup;
public GameObject PrzedmiotyGroup;
public List<GameObject> zwierzeta_sprites;
public List<GameObject> jedzenia_sprites;
public List<GameObject> przedmioty_sprites;
public static DateTime oldDate;
Camera mainCamera;
public SaveState saveState;
void Start()
{
mainCamera = Camera.main;
FillArrays();
}
public void Save()
{
Debug.Log("Saving.");
SaveSpriteArray("zwierze", zwierzeta_sprites);
SaveSpriteArray("przedmiot", przedmioty_sprites);
SaveSpriteArray("jedzenie", jedzenia_sprites);
PlayerPrefs.SetInt("pieniazki", saveState.GetPieniazki());
PlayerPrefs.SetInt("HayAmount", saveState.GetHayAmount());
PlayerPrefs.SetInt("HayMax", saveState.GetHayMax());
PlayerPrefs.SetInt("FruitAmount", saveState.GetFruitAmount());
PlayerPrefs.SetInt("FruitMax", saveState.GetFruitMax());
//time:
PlayerPrefs.SetString("sysString", System.DateTime.Now.ToBinary().ToString());
PlayerPrefs.SetInt("First", 1);
}
public void SaveSpriteArray(string saveName, List<GameObject> sprites)
{
for (int i = 0; i < sprites.Count; i++)
{
if (sprites[i].activeSelf)
{
PlayerPrefs.SetInt(saveName + i, 1);
}
else
{
PlayerPrefs.SetInt(saveName + i, 0);
}
}
}
public void Load()
{
Debug.Log("Loading.");
//wczytanie czasu:
long temp = Convert.ToInt64(PlayerPrefs.GetString("sysString"));
oldDate = DateTime.FromBinary(temp);
Debug.Log("oldDate: " + oldDate);
//wczytywanie aktywnych sprite'ow
LoadSpriteArray("zwierze", zwierzeta_sprites);
LoadSpriteArray("przedmiot", przedmioty_sprites);
LoadSpriteArray("jedzenie", jedzenia_sprites);
saveState.SetPieniazki(PlayerPrefs.GetInt("pieniazki"));
saveState.SetHayAmount(PlayerPrefs.GetInt("HayAmount"));
saveState.SetHayMax(PlayerPrefs.GetInt("HayMax"));
saveState.SetFruitAmount(PlayerPrefs.GetInt("FruitAmount"));
saveState.SetFruitMax(PlayerPrefs.GetInt("FruitMax"));
mainCamera.GetComponent<UpdateMoney>().MoneyUpdate();
}
public void LoadSpriteArray(string saveName, List<GameObject> sprites)
{
for (int i = 0; i < sprites.Count; i++)
{
if (PlayerPrefs.GetInt(saveName + i) == 1)
{
sprites[i].SetActive(true);
}
else
{
sprites[i].SetActive(false);
}
}
}
private void FillArrays()
{
//find children
foreach (Transform child in ZwierzetaGroup.transform)
{
zwierzeta_sprites.Add(child.gameObject);
}
foreach (Transform child in PrzedmiotyGroup.transform)
{
przedmioty_sprites.Add(child.gameObject);
}
foreach (Transform child in JedzeniaGroup.transform)
{
jedzenia_sprites.Add(child.gameObject);
}
}
}
Below is a chunk of script attached to the main camera (probably a mistake). SaveManager GameObject with Script is attached to this one in inspector. This script is pretty big, so I'll skip the parts that I don't find relevant.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class ManageEncouters: MonoBehaviour
{
DateTime currentDate;
public int First;
public SaveState saveState;
public SaveManager saveManager;
public HayBar hayBar;
public FruitBar fruitBar;
public GameObject[] jedzenia_sprites;
void Start()
{
}
void OnApplicationPause(bool pauseStatus)
{
if (!pauseStatus)
{
currentDate = System.DateTime.Now;
//Sprawdzanie czy jest to piersze uruchomienie gry (brak zapisu)
First = PlayerPrefs.GetInt("First");
if (First == 0)
{
Debug.Log("First time in app.");
RandomiseAnimals();
SaveManager.oldDate = currentDate;
hayBar.SetHayMax(1);
hayBar.SetHay(0);
fruitBar.SetFruitMax(1);
fruitBar.SetFruit(0);
saveState.SetPieniazki(100);
this.GetComponent<UpdateMoney>().MoneyUpdate();
}
else
{
Debug.Log("Not the first time in app.");
saveManager.Load();
}
if (TimeInSeconds(currentDate, SaveManager.oldDate) > 12)
{
Debug.Log("It's been more than 12 seconds sience last time.");
EatFood(currentDate, SaveManager.oldDate);
RandomiseAnimals();
}
else
{
Debug.Log("It's been less than 12 seconds sience last time.");
}
}
if (pauseStatus)
{
saveManager.Save();
}
}
private int TimeInSeconds(DateTime newD, DateTime oldD)
{
TimeSpan difference = newD.Subtract(oldD);
int seconds = (int)difference.TotalSeconds;
return seconds;
}
}
Below is the error I get, I don't know how to copy the text, so it's an Image.
I'm pretty sure that what you have here is a timing issue.
OnApplicationPause
Note: MonoBehaviour.OnApplicationPause is called as a GameObject starts. The call is made after Awake. Each GameObject will cause this call to be made.
So to me this sounds like it might be called when your SaveManager is not yet initialized, in particular the mainCamera.
I think you could already solve the issue by moving the initialization into Awake instead
private void Awake()
{
mainCamera = Camera.main;
FillArrays();
}
In general my little thumb rule is
use Awake wherever possible. In particular initialize everything where you don't depend on other scripts (initialize fields, use GetComponent, etc)
use Start when you need other scripts to be initialized already (call methods on other components, collect and pass on instances of some prefabs spawned in Awake, etc)
This covers most of cases. Where this isn't enough you would need to bother with the execution order or use events.
At the start, the object adds a link to it to the list. Then when i click on this object, i need to get index reference of this object. How to do it?
Little example what i need:
public static List<GameObject> myObjects = new List<GameObject> ();
public GameObject ObjectA; //this is prefab
void Start(){
for (int i = 0; i < 10; i++) {
GameObject ObjectACopy = Instantiate (ObjectA);
myObjects.Add (ObjectACopy);
}
}
ObjectA script:
void OnMouseDown(){
Debug.Log(//Here i need to return index of this clicked object from the list);
}
Loop through the Objects List. Check if it matches with the GameObject that is clicked. The GameObject that is clicked can be obtained in the OnMouseDown function with the gameObject property. If they match, return the current index from that loop. If they don't match, return -1 as an error code.
void OnMouseDown()
{
int index = GameObjectToIndex(gameObject);
Debug.Log(index);
}
int GameObjectToIndex(GameObject targetObj)
{
//Loop through GameObjects
for (int i = 0; i < Objects.Count; i++)
{
//Check if GameObject is in the List
if (Objects[i] == targetObj)
{
//It is. Return the current index
return i;
}
}
//It is not in the List. Return -1
return -1;
}
This should work but it is better that you stop using the OnMouseDown function and instead use the OnPointerClick functions.
public class Test : MonoBehaviour, IPointerClickHandler
{
public static List<GameObject> Objects = new List<GameObject>();
public void OnPointerClick(PointerEventData eventData)
{
GameObject clickedObj = eventData.pointerCurrentRaycast.gameObject;
int index = GameObjectToIndex(clickedObj);
Debug.Log(index);
}
int GameObjectToIndex(GameObject targetObj)
{
//Loop through GameObjects
for (int i = 0; i < Objects.Count; i++)
{
//Check if GameObject is in the List
if (Objects[i] == targetObj)
{
//It is. Return the current index
return i;
}
}
//It is not in the List. Return -1
return -1;
}
}
See this post for more information on OnPointerClick.
EDIT:
Objects.IndexOf can also be used for this. This answer is here to make you understand how to do it yourself so that you can solve similar issues in the future.