0
\$\begingroup\$

I have a state machine that controls my enemy AI. Each AI has a target which may be a Player, an obstacle, a shell, or even a Vector2 position.

I'm trying to abstract my "target" member, and my first thought was to use GameObject, then downcast to whichever type I need for different enemies. Examples below:

class StateController: MonoBehavior {
    GameObject target;
    FireAction action;

    Update() {
        action.Fire(this);
    }
}

class FireAction: ScriptableObject {
    public abstract void Fire(StateController c);
}

//Examples of different fire scripts:
class FireAtPlayer: FireAction {
    public override void Fire(StateController c) {
        Player p = c.target as Player;
        //--use properties of Player object--
    }
}

class FireAtPoint: FireAction {
    public override void Fire(StateController c) {
        GameObject t = c.target as Player;
        //--use properties of GameObject object--
    }
}

As you can see, my Fire methods would cast the target object at every call to Update. Will this negatively affect my game's performance? Note that I know for a fact what type of object the target is, so I'm not guessing when I'm casting. Thank you in advance of your help!

\$\endgroup\$
7
  • 1
    \$\begingroup\$ What does your profiling so far have to say about this? \$\endgroup\$
    – DMGregory
    Mar 7, 2019 at 19:13
  • \$\begingroup\$ I haven't implemented it yet. I didn't want to go through all the effort if it was pointless. I'm also in the market for a different solution. \$\endgroup\$
    – retrovius
    Mar 8, 2019 at 1:11
  • 1
    \$\begingroup\$ Then I recommend doing your profiling first, and asking here if it proves to be a real problem that you need outside help to solve. There are enough variables in play that this might not be an issue at all for the context of your game and its specific needs: only you have the details to be able to profile it accurately to determine whether it's fast enough for your use case. \$\endgroup\$
    – DMGregory
    Mar 8, 2019 at 1:15
  • 1
    \$\begingroup\$ How often does that cast happen? If it is once per frame or less, then I would not bother. Casting is not free, but it isn't that expensive either. There is this answer on stackoverflow which says it's almost free, but keep in mind that it's based on the Microsoft.NET Common Language Runtime - Unity uses different C# implementations in many build targets. \$\endgroup\$
    – Philipp
    Mar 8, 2019 at 16:08
  • \$\begingroup\$ Regarding the line GameObject t = c.target as Player; If I read your code correctly, c.target already is a GameObject. So you are downcasting it to Player just to immediately upcast it to GameObject again. That seems to be a bit pointless. The only reason I could see why one might do that is because as Player will return null when it's not a player, so you might be using it to detect if you are dealing with a player. But in that case you might rather want to use if (c.target is Player). \$\endgroup\$
    – Philipp
    Mar 8, 2019 at 16:16

3 Answers 3

2
\$\begingroup\$

What other's have said is correct. It likely won't cause issue. This is partially a problem with oop in general. The non casting solutions are not easy to maintain and work with. An alternative component based approach might be better, but since you're using oop i'll go from there. There's two alternatives that come to mind

  1. Duplicate your pipeline for each subtype. For example, let's say I have BaseCharacter, and Enemy and Player which inherit BaseCharacter. I can have something like this
public class SpawnManager : Monobehaviour {
  public event Action CharacterSpawned;
  public event Action PlayerSpawned;
  public event Action EnemySpawned;

  public void SpawnCharacter(BaseCharacter char) {
    Instantiate(char)
    if (CharacterSpawned != null) CharacterSpawned.Invoke();
  }

  public void SpawnPlayer(Player player) {
    SpawnCharacter(player);
    if (PlayerSpawned != null) PlayerSpawned.Invoke();
  }

  public void SpawnEnemy(Enemy enemy) {
    SpawnCharacter(enemy);
    if (EnemySpawned != null) EnemySpawned.Invoke();
  }
}
  1. Another alternative to not cast is to just have a union-like struct which as long as it's holding pointer references, is only taking up sizeof(pointer) * N where N is the number of subclasses
public struct SpawnEvent {
  public enum Type { Player, Enemy };
  public Type Type;
  public Player player; // null if enemy
  public Enemy enemy; // null if player
}

Then check the enum and get the correct field as needed. Again none of these approaches are perfect, nor is casting, but if you stick with OOP you'll find this is one of the places it falls short. You can look into component based design, if you're really not satisfied, but i'm not as well versed in that to offer much advice.

\$\endgroup\$
1
\$\begingroup\$

If you can avoid casting frequently casting then do. However if you do not find it causes a reasonable performance hit then don't worry.

Also it is not possible to cast GameObject to Player as you cannot inherit GameObject yourself. You should be using GetComponent() to get components from GameObjects.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Oh, I understand what you mean. I wasn't quite thinking clearly, I would use get component. I think I'll leave the question up, so that people can learn from it, but the question is no longer valid because of my misunderstanding. \$\endgroup\$
    – retrovius
    Mar 9, 2019 at 20:25
0
\$\begingroup\$

I wasn’t aware that you could cast a GameObject into a component type on that GameObject and it would work. In fact I don’t think it will, will it?

Even if it did, it would be akin to calling GetComponent, which is an expensive function, every frame, which is generally accepted to be a Bad Idea.

From Unity’s documentation:

In Unity, it is common is to call GetComponent() to access components [repeatedly. ...] This code works, but it is inefficient due to the repeated GetComponent() call.

A more efficient solution would likely involve a bit more polymorphism, although it would require a bit of restructuring from your presented design, and even that has its own drawbacks. As DMGregory suggested, you won’t know if your solution is sufficient or requires some rework until you try it out. Just keep in mind that your solution is simple, but not particularly efficient.

\$\endgroup\$
2
  • \$\begingroup\$ I assumed OP meant using a variable of type UnityEngine.Object to store either a game object or component reference directly, but just used the wrong "object" when writing the example code. Another good reason to test the code first: it will catch errors like that. ;) \$\endgroup\$
    – DMGregory
    Mar 8, 2019 at 14:21
  • \$\begingroup\$ That makes a lot more sense now \$\endgroup\$
    – Ed Marty
    Mar 8, 2019 at 23:33

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .