I have an abstract class called UnitAI that all my units inherit from: and it has quite a bit of information so I decided to separate the abstract class into multiple smaller classes that are partial to it.
I did this because I wanted to easily remember what I’m inheriting from (I know there’s controversy on inheriting but since I work on this alone, it’s quite faster for me to do it this way).
Anyway what I’d like to know is how could I implement abstract methods that my AI has to implement in the classes that my UnitAI is composed of.
For instance here, in my CombatInfo I’d like to force the class that inherits from UnitAI to implement its personal Attack() method.
Here’s how it looks:
// in 1 .cs file
public abstract partial class UnitAI : MonoBehaviour
{
public TargetInfo InfoTarget;
public TerrainInfo InfoTerrain;
public CombatInfo Combat;
void Awake()
{
InfoTarget = new InfoTarget(this);
InfoTerrain = new InfoTerrain(this);
Combat = new CombatInfo(this);
}
void Update()
{
InfoTarget.UpdateTarget();
InfoTerrain.UpdateTerrain();
Combat.UpdateCombat();
}
}
// in other .cs files
public abstract partial class UnitAI
{
[Serializable]
public class CombatInfo
{
public abstract void Attack(); // This won't compile! The CombatInfo class is not abstract! But in the idea it's what I want
public virtual void Attack() // This compiles but it's not what I want. I want to force them to implement this method!
{
// Nothing in the attack method, it depends entirely on who inherits from the UnitAI class.
}
}
}
Well technically speaking, you want them to ALSO inherit from CombatInfo and implement Attack on that.
Just because the class is nested in UnityAI doesn’t mean the CombatInfo really has anything to do with UnityAI (as classes go). It is still its own class.
I’m not sure what partial classes have to do with this, nor why you’re using them…
There’s also an added layer of issue here since CombatInfo is serializable and you expect it to be serialized by Unity since it’s a public field of ‘UnitAI’. You can’t inherit from it and have serialization work correctly since the way Unity does serialization… it unfortunately doesn’t work that way.
…
What I’d do instead is make ‘Attack’ a member of UnityAI, and have the method take a ‘CombatInfo’ as a parameter:
public abstract class UnitAI : MonoBehaviour
{
public TargetInfo InfoTarget;
public TerrainInfo InfoTerrain;
public CombatInfo Combat;
void Awake()
{
InfoTarget = new InfoTarget(this);
InfoTerrain = new InfoTerrain(this);
Combat = new CombatInfo(this);
}
void Update()
{
InfoTarget.UpdateTarget();
InfoTerrain.UpdateTerrain();
Combat.UpdateCombat();
}
public abstract void Attack(CombatInfo info);
}
public class CustomUnitAI : UnitAI
{
public override void Attack(CombatInfo info)
{
//do whatever based on CombatInfo
}
}
Though honestly, I’m not sure what CombatInfo even does since it has no fields.
…
What is it you’re exactly trying to do anyways? I don’t mean from a programming syntax perspective, but rather your end goal? What is it you’re attempting to accomplish by having this forced implementation?
The example I gave was not exhaustive, I have a lot of stuff in CombatInfo.
I have a lot of redundant behaviour between all of my AI’s and I’d like to be able to create my AI as fast as possible while retaining the possiblity of changing some of their core mechanics. (mostly override would do that for me).
What I’m doing would not be complicated if I didn’t want the methods the Unit inherits to be wrapped in a class.
If I just inherited from UnitAI I would have 1 big class (a 2000 line long class). I wouldn’t be able to remember what kind of methods I have in that class anyway.
That’s why I separated UnitAI into more classes, I want to be able to write “Combat.” and have auto-completion show me all of the methods that the CombatInfo class has. That’s the goal.
So why not actually split them into more classes, rather than these partial classes with wrapped inner-classes.
Like actually have a “UnitAI” component, and then multiple other components for “UnitAILogic”. Then you add what logic components you want included with the UnitAI.
A composition approach, rather than an inheritance approach.
Think of it like Rigidbody & Collider. You don’t have it all wrapped in one class. But rather instead you add a Rigidbody (which is the physics logic), and you add a Collider (which represents the shape of the physics).
Here’s my AI controller for a boss I have…
Here I have MobBrain (like UnitAI) which is the basic code for how AI works. It allows for states for the AI which I attach to GameObjects inside the brain. I enable the state that is currently active.
Note that I break apart the logic. For example the ‘PursuitLogic’ script, well a lot of mobs pursue. That code is written once in that script, and I attach it to any mob that needs to be capable of pursuing. I ‘composite’ it with other logic scripts, like ‘CustodianConsiderChargingLogic’ to make behaviour unique to this specific mob type.
That would imply that these components that you have actually have more than helper methods in them. Most of my code simply has methods that will generally be used in an AI.
It almost has no logic compared to what your components do.
For example: my TargetInfo has simple methods related to what the current target of the AI is.
The important difference between what you have and what I want is that my creatures won’t have as much shared behaviour as yours do most likely. So I wouldn’t need different versions of Pursuit Logic, because most likely there would be too many variations between each monster anyway.
Additionally, I do not want to rely as much on complex hierarchies of components, I find it bug prone. I’d like to have to look as much as possible in one place to find bugs. I think it’s a good system that you have but just not for what I want right now.
I think that for what I need it’s the correct idea to have it done through inheritance but I must find a way to wrap these various methods into classes somehow.
Here’s my TargetInfo class for example:
using System;
using UnityEngine;
public abstract partial class UnitAI
{
/// <summary>
/// Has useful informations about the Target.
/// You can set the target to whatever you want.
/// If the target dies, use the OnTargetIsNull to decide who the next Target should be.
/// </summary>
[Serializable]
public class TargetInfo
{
public TargetInfo(UnitAI _agent)
{
Agent = _agent;
}
private readonly UnitAI Agent;
public Controller2 TargetController2;
[HideInInspector]
public Rigidbody2D TargetRb2D;
/// <summary>
/// This is the player, but you can set it to something else.
/// </summary>
public Transform Target
{
get { return Agent.Target; }
set { Agent.Target = value; }
}
/// <summary>
/// Does AI have a target?
/// </summary>
public bool HasTarget
{
get { return Target != null; }
}
/// <summary>
/// Am I facing my current Target?
/// </summary>
public bool IsFacingTarget { get; private set; }
/// <summary>
/// Is the target to my right (+1) or to my left (-1)
/// The current facing direction of the AI is irrelevant in this.
/// </summary>
public int TargetDir { get; private set; }
public float DistanceToTarget { get; private set; }
/// <summary>
/// Returns the X distance from the AI to the Target.
/// </summary>
public float XDistanceToTarget { get; private set; }
/// <summary>
/// Returns the X distance from the AI to the Target.
/// This can be positive or negative.
/// </summary>
public float XToTarget { get; private set; }
/// <summary>
/// Returns the Y distance from the AI to the Target.
/// </summary>
public float YDistanceToTarget { get; private set; }
/// <summary>
/// Returns the Y distance from the AI to the Target.
/// This can be positive or negative.
/// </summary>
public float YToTarget { get; private set; }
/// <summary>
/// Returns Target position.
/// </summary>
public Vector3 TargetPos { get; private set; }
/// <summary>
/// Returns Target position.
/// </summary>
public Vector2 TargetPos2D { get; private set; }
public float SignedAngleToTarget(Vector2 _start)
{
return Vector2.SignedAngle(_start, Target.transform.position - Agent.transform.position);
}
public float PositiveAngleToTarget(Vector2 _start)
{
return Vector2.Angle(_start, Target.transform.position - Agent.transform.position);
}
/// <summary>
/// You don't need to call this in AIAgents implementations.
/// Don't do it or ask Jeremy.
/// </summary>
public void UpdateInfo()
{
if (Target == null)
{
OnTargetIsNull();
}
if (Target == null) return;
IsFacingTarget = FacingTheTarget();
TargetPos = Target.transform.position;
TargetPos2D = TargetPos;
var result = TargetToTheRight();
TargetDir = result ? +1 : -1;
DistanceToTarget = DistToTarget();
XToTarget = XVectorToTarget();
XDistanceToTarget = Mathf.Abs(XToTarget);
YToTarget = YVectorToTarget();
YDistanceToTarget = Mathf.Abs(YToTarget);
}
private void OnTargetIsNull()
{
var player = Player.Instance;
if (player)
Target = player.transform;
else
Debug.Log(Agent.transform.name + " is looking for a Target but didn't find it");
TargetController2 = Target.GetComponent<Controller2>();
TargetRb2D = Target.GetComponent<Controller2>().Rb2D;
// TODO: Create an event so that the the implementation of the AI can decide which target to pick when his target is null.
}
private bool FacingTheTarget()
{
if (TargetToTheRight() && Agent.Control.FacingDirection == 1) return true;
else if (TargetToTheRight() == false && Agent.Control.FacingDirection == -1) return true;
else return false;
}
private bool TargetToTheRight()
{
return Target.position.x - Agent.transform.position.x > 0;
}
/// <summary>
/// Creates a Line SoloRaycast checking for Environment layer from AIAgent to Target to see if there's something in the way.
/// </summary>
public bool IsTargetInSight()
{
//if (Target == null) return null;
var dirToTarget = Target.position - Agent.transform.position;
var result = Physics2D.Raycast(Agent.transform.position, dirToTarget, dirToTarget.magnitude,
GameConst.EnvironmentMask);
return !result;
}
private float DistToTarget()
{
return Vector2.Distance(Target.position, Agent.transform.position);
}
private float XVectorToTarget()
{
return Target.position.x - Agent.transform.position.x;
}
private float YVectorToTarget()
{
return Target.position.y - Agent.transform.position.y;
}
}
}
You’re effectively doing composition, since you have distinct classes for each aspect… but you’re restricting your self to inheritance by attempting to force them to inherit from UnitAI when doing it.
Although, there’s a disconnect since inheriting from UnitAI does not force inheriting from CombatInfo. Since that’s not how inner classes work. And of course there’s the serialization issue through a second wrench into the problem.
What do you expect the difference to be between if CombatInfo is an inner class of UnitAI, and if CombatInfo is not an inner class of UnityAI? Because from a C# perspective, it’s just namespace.
It’s true and I understand that I’m doing composition but I wouldn’t do composition if I didn’t need the auto-complete feature. I really don’t need composition here, I just need to wrap methods into different “groups” of methods if you will.
I’m not saying you should do exactly what I did… I was just showing it for demonstrative purposes.
What I’m saying is do something like this:
public class UnitAI : MonoBehaviour
{
public TargetInfo InfoTarget;
public TerrainInfo InfoTerrain;
public CombatInfo Combat;
void Awake()
{
InfoTarget = new InfoTarget(this);
InfoTerrain = new InfoTerrain(this);
if(Combat == null) Combat = this.GetComponent<CombatInfo>();
}
void Update()
{
InfoTarget.UpdateTarget();
InfoTerrain.UpdateTerrain();
Combat.UpdateCombat();
}
}
// in other .cs files
public abstract class CombatInfo : MonoBehaviour
{
public abstract void Attack();
}
Yes, but C# fundamentally doesn’t do what you are trying to make it do.
…
[edit]
Note, you can also use the [RequireComponent(typeof(CombatInfo))] attribute on the UnitAI to make sure that when someone adds a UnitAI the editor yells at them they need a CombatInfo script attached as well for it to work. Getting that ‘forced’ nature you may desire.