Thursday, December 19, 2019

Unity: Assign GameObject to Class that hasn't been yet instantiated


I'm creating a game where I have an Entity class that contains basic entity info (pos, health,..) and some functions. Enemies and Player later on inherit from this class.


Public class Entity {


public Vector2 position;
public float health;
,...

Lets say that I have a function like this inside the Entity class:


public GameObject movingIntoEnemy(Vector2 movingToPoint)
{
foreach (GameObject go in GameObject.Find("GameMap").GetComponent().enemyList)
{
if (go.GetComponent().enemy.position == movingToPoint)

{
return go;
}
}
return null;
}

This function looks for a GameObject called "GameMap" and then cycles through enemies in it's list. If an enemy is found, it returns it's GameObject. Using GameObject.Find() is pretty ineffective, so what would be a better approach to this? I mean, I can't just easily use


public GameObject GameMap;


since the code is in a class, and this class needs to be instantiated first in my player/enemy scripts.


My question is - how do I attach gameObjects to this class before it is even created? What is the appropriate approach to this? I guess that I might pass these GameObjects to the class through it's constructor, but it would make things much more complicated.


Thanks in advance ;)



Answer



Obviously we can't do anything to a class instance that hasn't yet been instantiated, because by definition it doesn't exist yet.


The real question here is "how can a class instance created at runtime get access to its dependencies, in Unity specifically?"


There are a couple of general approaches to this:




  1. Make the GameMap globally accessible so each Entity instance can reach out and find it. (You're already doing a flavour of this)





  2. Provide a GameMap instance to each Entity when it's spawned. (This is called dependency injection)




For 1, we have options like...


a) Each instance searches for its dependencies, using GameObject.Find() as you're doing now, or FindObjectOfType() to shortcut straight to grabbing the component you want like so:


GameMap map = (GameMap)FingObjectOfType(typeof(GameMap);

As you point out, this isn't ideal, but it's also not a grave sin as long as you cache the reference for reuse via a member variable, so you're not searching for it every frame/every event.



b) Implement the Singleton Pattern. There are many ways to skin this cat, but a common theme is a static method to get an instance (and possibly construct it on-demand if it's absent from the project):


public class GameMap : Monobehaviour
{
private static GameMap _instance;
public static GameMap GetInstance()
{
return _instance;
}

void Awake()

{
_instance = this;
}

...

Now any code elsewhere in your scene can call:


var map = GameMap.GetInstance();

to get an instance to the one GameMap quite cheaply.



Where Singletons tend to start running into trouble is...



  • When you change scenes and your Singleton is deleted (fix with DontDestroyOnLoad)

  • When you chance scenes with DontDestroyOnLoad and there's already a Singleton in the new scene, so now you have two (fix by self-destructing if there's already an instance)

  • When you try to use the Singleton in a scene without one, or before it's Awake (fix by constructing on demand)

  • When you want to extend your game so there can be multiple GameMaps at once (may require substantial refactoring - this applies to any of the "global access" strategies)


None of this is insurmountable, you just need to be on the lookout for it. But if we're using static members anyway, we could sidestep some of the complexity and just...


c) Make the shared dependency itself static. You lose the ability to put it in the hierarchy or manipulate it in the inspector, but if you so choose you can farm these responsibilities out to auxiliary objects that are either constructed by the GameMap as needed, or already exist in the scene and register themselves with the GameMap on start.





On to #2, dependency injection...


When you create your Entity instances, tell them which GameMap they should use.


If you're not using a Monobehaviour, this is as straightforward as passing an argument to their constructor, as you describe in the question:


var myEntity = new Entity(myGameMap);

If you are inheriting from Monobehaviour, then you wouldn't use a constructor, but instead a setter after instantiating the object or attaching the component:


var myEntity = Instantiate(myEntityPrefab) as Entity;

myEntity.SetGameMap(myMap);


Either way, the Entity caches the map it's passed, and any code that runs after [construction / Awake()] will know which map to use.


This approach scales to uses where you might eventually introduce multiple GameMaps, so the Entity objects don't need to know how to request the right one - it's pushed in from whatever higher-level system created them (which usually has more knowledge of the context in which it's creating the Entity)


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...