Introduction
The Core is meant to be a hub where an entity can get access to a variety of components to help it interact with the environment. Currently our Core gives the entity, such as our player and enemies, access to the following components: Movement Core Component, CollisionSenses Core Component, Combat Core Component, and Stats Core Component. As it currently stands, these components are somewhat required by every entity. Even if an entity were to not make use of the combat component, the Core still knows about it. The Core knows about every component. Yes, if an entity does not use a certain component we do not need to add the Game Object and related scripts, but this is not an elegant solution. Today we are going to solve this by making the way the Core handles Core Components more dynamic.
Updating the Way the Core Stores References
Let’s consider the Core.cs script.
Core.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Core : MonoBehaviour
{
public Movement Movement
{
get => GenericNotImplementedError.TryGet(movement, transform.parent.name);
private set => movement = value;
}
public CollisionSenses CollisionSenses
{
get => GenericNotImplementedError.TryGet(collisionSenses, transform.parent.name);
private set => collisionSenses = value;
}
public Combat Combat
{
get => GenericNotImplementedError.TryGet(combat, transform.parent.name);
private set => combat = value;
}
public Stats Stats
{
get => GenericNotImplementedError.TryGet(stats, transform.parent.name);
private set => stats = value;
}
private Movement movement;
private CollisionSenses collisionSenses;
private Combat combat;
private Stats stats;
private List components = new List();
private void Awake()
{
Movement = GetComponentInChildren();
CollisionSenses = GetComponentInChildren();
Combat = GetComponentInChildren();
stats = GetComponentInChildren();
}
public void LogicUpdate()
{
foreach (ILogicUpdate component in components)
{
component.LogicUpdate();
}
}
public void AddComponent(ILogicUpdate component)
{
if (!components.Contains(component))
{
components.Add(component);
}
}
}
We will start by removing the hardcoded CoreComponent references:
// {...} Represents the getters and setters
public Movement Movement { ... }
public CollisionSenses CollisionSenses { ... }
public Combat Combat { ... }
public Stats Stats { ... }
private Movement movement;
private CollisionSenses collisionSenses;
private Combat combat;
private Stats stats;
And then replacing it with a list instead:
public readonly List CoreComponents = new List();
Here, we make use of the readonly keyword to protect the list reference from accidentally being changed at runtime.
Now we have two options to populate this list. We can either make the Core look at all its children and add all the CoreComponents that it finds to the list, or each core component can look for its parent core game object and add themselves to the list. We will be applying the second approach as the core components are already adding themselves to a list on the Core for Logic Update purposes.
In the previous part (34), we created a method in the Core called AddComponent that core components use to add themselves to the components list used to call the LogicUpdate function on all the core components. Let’s change this method to take in a type of CoreComponent as an input parameter instead of the ILogicUpdate type. We can also then change the method to use the CoreComponents list instead of the components list.
public void AddComponent(CoreComponent component)
{
if (!CoreComponents.Contains(component))
{
CoreComponents.Add(component);
}
}
This means we no longer need theย components list, so let’s get rid of it. We can then also change the foreach loop in the LogicUpdate function to make use of the newย CoreComponents list and CoreComponent instead of ILogicUpdate.
public void LogicUpdate()
{
foreach (CoreComponent component in CoreComponents)
{
component.LogicUpdate();
}
}
As we only altered the AddComponent function, and the core component is already adding itself using the this keyword, there are not changes we need to make in CoreComponent.cs.
Retrieving Core Components
Now without the hardcoded CoreComponent references, we need a new way for our states and other core components to access the components that they need. We will do this with a Generic method that will return the correct core component we are looking for. In Core.cs we can do the following:
Start by importing the Linq namespace into the project.
using System.Linq;
And then writing the following generic method. Don’t worry, we’ll walk through it line by line.
public T GetCoreComponent() where T:CoreComponent
{
// Search list for the desired component
var comp = CoreComponents
.OfType()
.FirstOrDefault();
// Check if component was found. Log warning if not
if(comp == null)
{
Debug.LogWarning($"{typeof(T)} not found on {transform.parent.name}");
}
// Return the component
return comp;
}
Let’s start with line 1, the method declaration.
public T GetCoreComponent() where T:CoreComponent
Here, T represents our generic type. By writing public T we are saying that this method is going to return something that has the type of whatever T is. After our method name we haveย <T>. This is how we specify that this is a generic method and that T is the type to use. Finally we place a constraint on T with where T:CoreComponent. This means that T can no longer be just anything but instead must be of type CoreComponent or any type that inherits from CoreComponent. We do this because it does not make sense to be able to specify something like the type int when we are looking for a component.
Next let’s consider lines 4 through 6.
var comp = CoreComponents
.OfType()
.FirstOrDefault();
This part of the code is responsible for getting what we are looking for from the CoreComponents list. We start off by declaring a variable comp, this is where we will store the component to return. We set this equal to CoreComponents then:
.OfType<T>(). This is a method provided by the Linq library. It takes the list and filters it to only contain objects that are of the type T. This T is the same T that we use in the method declaration. Meaning whatever type we specify for the method is the type that will be used as the filter for the list. At this point we still don’t have a single object as the method would have returned a collection of all the objects that matched that type, even if there was only one, or even none. Therefor we need:
.FirstOrDefault();. This method will look at that collection and return the first object in the sequence. If the collection is empty, it will instead return a default value. Because we are making use of a class,ย CoreComponent, as our type, the default value would be null.
That brings us to lines 9 through 12.
if(comp == null)
{
Debug.LogWarning($"{typeof(T)} not found on {transform.parent.name}");
}
If the returned collection was empty, that means our entity does not have the core component that is trying to be accessed. Therefore we just want to gently let ourselves, or whoever else is working on the project, know that the code they have written is trying to use a CoreComponent that does not exist on the entity, so they will need to implement it.
Finally, in line 15 we simply return the result of the query, be it a component or null.
Using the Core Components
Unfortunately when we go back to Unity we will be greeted by this sight:
This is because everything that relied on those hardcoded references are now broken. So what we need to do is go through each error and replace things like:
Core.Movement… with Core.GetCoreComponent()…
However, for the same reason that we don’t call Unity’s GetComponent at runtime and instead call it in the Awake method, we don’t want to call our method multiple times per frame for different core components and multiple entities. Even though the lists where we look for these core components are small, it would be more efficient and performant to cache these references where they are actually needed.
So let’s start addressing these errors. Consider the PlayerGroundedState.cs:
PlayerGroundedState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerGroundedState : PlayerState
{
protected int xInput;
protected int yInput;
protected bool isTouchingCeiling;
private bool JumpInput;
private bool grabInput;
private bool isGrounded;
private bool isTouchingWall;
private bool isTouchingLedge;
private bool dashInput;
public PlayerGroundedState(Player player, PlayerStateMachine stateMachine, PlayerData playerData, string animBoolName) : base(player, stateMachine, playerData, animBoolName)
{
}
public override void DoChecks()
{
base.DoChecks();
isGrounded = core.CollisionSenses.Ground;
isTouchingWall = core.CollisionSenses.WallFront;
isTouchingLedge = core.CollisionSenses.LedgeHorizontal;
isTouchingCeiling = core.CollisionSenses.Ceiling;
}
public override void Enter()
{
base.Enter();
player.JumpState.ResetAmountOfJumpsLeft();
player.DashState.ResetCanDash();
}
public override void Exit()
{
base.Exit();
}
public override void LogicUpdate()
{
base.LogicUpdate();
xInput = player.InputHandler.NormInputX;
yInput = player.InputHandler.NormInputY;
JumpInput = player.InputHandler.JumpInput;
grabInput = player.InputHandler.GrabInput;
dashInput = player.InputHandler.DashInput;
if (player.InputHandler.AttackInputs[(int)CombatInputs.primary] && !isTouchingCeiling)
{
stateMachine.ChangeState(player.PrimaryAttackState);
}
else if (player.InputHandler.AttackInputs[(int)CombatInputs.secondary] && !isTouchingCeiling)
{
stateMachine.ChangeState(player.SecondaryAttackState);
}
else if (JumpInput && player.JumpState.CanJump())
{
stateMachine.ChangeState(player.JumpState);
}else if (!isGrounded)
{
player.InAirState.StartCoyoteTime();
stateMachine.ChangeState(player.InAirState);
}else if(isTouchingWall && grabInput && isTouchingLedge)
{
stateMachine.ChangeState(player.WallGrabState);
}
else if (dashInput && player.DashState.CheckIfCanDash() && !isTouchingCeiling)
{
stateMachine.ChangeState(player.DashState);
}
}
public override void PhysicsUpdate()
{
base.PhysicsUpdate();
}
}
In the code above you’ll find all the lines that are currently causing errors highlighted.
Let’s start by creating a variable where we can cache the reference to the CollisionSenses core component.
private CollisionSenses collisionSenses;
Now the lines that are causing errors can be changed to:
isGrounded = collisionSenses.Ground; //Line 29
isTouchingWall = collisionSenses.WallFront; //Line 30
isTouchingLedge = collisionSenses.LedgeHorizontal; //Line 31
isTouchingCeiling = collisionSenses.Ceiling; //Line 32
Now we just need to cache the CollisionSenses core component reference. But where do we do that? We have multiple issues.
- Our states do not have an Awake method
- Even if our states had an Awake method, we cannot guarantee that each of our core components Awake methods would be called first. This means that the core component we are looking for might not have added itself to the list yet.
The simplest solution that we have that does not require us to alter a bunch of code in other places to ensure we get the right sequence of execution is to simply cache the reference at runtime the first time it is used. So let’s go ahead and alter our variable declaration:
private CollisionSenses CollisionSenses
{
get
{
if (collisionSenses)
{
return collisionSenses;
}
collisionSenses = core.GetCoreComponent();
return collisionSenses;
}
}
private CollisionSenses collisionSenses;
We declared another CollisionSenses variable, in this case with an uppercase C. We can then give it a getter. Inside the getter we are then first going to check if our collisionSenses field is null or not with if(collisionSenses). If collisionSenses is not null, meaning we have a reference set, we are going to return it. However, if it is null it means we need to set the reference as shown in line 10 and then return it.
Everywhere we had errors before in this file need to be changed again to make use of the new CollisionSenses variable:
if (CollisionSenses)
{
isGrounded = CollisionSenses.Ground;
isTouchingWall = CollisionSenses.WallFront;
isTouchingLedge = CollisionSenses.LedgeHorizontal;
isTouchingCeiling = CollisionSenses.Ceiling;
}
Notice that we have now wrapped these check in an if statement as we are trying to look at properties that potentially do not exist if GetCoreComponent were to return null. This stops the game from throwing a whole bunch of errors were this to happen, allowing us to more easily track down the important warning message telling us we forgot to add a certain core component to an entity.
Now, the whole getter setup we have is quite long. I would hate to now have to go write that our for every core component that every state uses. So how can we make it shorter?
Let’s make use of the null-coalescing operator, ‘??‘. The whole block can then be changed to a single line:
private CollisionSenses CollisionSenses { get => collisionSenses ?? core.GetCoreComponent(); }
So what the new operator does is first evaluate the left hand side. If it is not null, it will return that whatever is on that side. If it is null, it will evaluate and return whatever is on the right hand side. However, in this case our collisionSenses variable is never assigned the core component. If we were using Unity 2021.2 or later the solution would we very simple and we could use ‘??=‘ instead as it would assign whatever the right returns to the left. However, I am still using version 2019.4. Maybe it is time to update, but we’ll take care of that later. For now, let’s see how we can solve this issue without it.
Let’s go back to our Core.cs file and create another version of our generic GetCoreComponent function that will this time take in a parameter that is the variable where we would like to store the reference.
public T GetCoreComponent(ref T value) where T:CoreComponent
{
value = GetCoreComponent();
return value;
}
Let’s walk through this again line by line. The method declaration is the same as before, except now we have included ref T value inside the brackets. As with any method, T value means we are going to pass it a variable of type T, and that variable is called value for use inside the method. However, when we pass a variable to a method we only pass a copy of the contents. Any changes we make to that content is not reflected in the original variable. However, if we add the ref keyword in front of the type, it indicates that we want to pass the variable by reference. This means that any changes we now make to this variable will be reflected in the original source.
After the method declaration we simply set value equal to whatever our original GetCoreComponent method would return. We then finally also return the value.
Back in our PlayerGroundedState.cs file, we can change the variable declaration to the following:
private CollisionSenses CollisionSenses { get => collisionSenses ?? core.GetCoreComponent(ref collisionSenses); }
Notice that all we changed is: we removed <CollisionSenses>, as the method can now infer the type from the variable we pass it, and then added ref collisionSenses as the input parameter for the method. This means that now, if movement is null, we will call GetCoreComponent and pass it a reference to our variable. The core component will then be stored in that reference. Finally, the value that GetCoreComponent returns will be returned by the getter. Next time we try to use the core it will use the actual movement variable.
Our Core.cs and PlayerGroundedState.cs should now look like this:
Core.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class Core : MonoBehaviour
{
public readonly List CoreComponents = new List();
public void LogicUpdate()
{
foreach (CoreComponent component in CoreComponents)
{
component.LogicUpdate();
}
}
public T GetCoreComponent() where T:CoreComponent
{
var comp = CoreComponents
.OfType()
.FirstOrDefault();
Debug.Log($"Type of comp found: {comp.GetType()}");
if (comp == null)
{
Debug.LogWarning($"{typeof(T)} not found on {transform.parent.name}");
}
return comp;
}
public T GetCoreComponent(ref T value) where T:CoreComponent
{
value = GetCoreComponent();
return value;
}
public void AddComponent(CoreComponent component)
{
if (!CoreComponents.Contains(component))
{
CoreComponents.Add(component);
}
}
}
PlayerGroundedState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerGroundedState : PlayerState
{
protected int xInput;
protected int yInput;
protected bool isTouchingCeiling;
private bool JumpInput;
private bool grabInput;
private bool isGrounded;
private bool isTouchingWall;
private bool isTouchingLedge;
private bool dashInput;
private CollisionSenses CollisionSenses { get => collisionSenses ?? core.GetCoreComponent(ref collisionSenses); }
private CollisionSenses collisionSenses;
public PlayerGroundedState(Player player, PlayerStateMachine stateMachine, PlayerData playerData, string animBoolName) : base(player, stateMachine, playerData, animBoolName)
{
}
public override void DoChecks()
{
base.DoChecks();
if (CollisionSenses)
{
isGrounded = CollisionSenses.Ground;
isTouchingWall = CollisionSenses.WallFront;
isTouchingLedge = CollisionSenses.LedgeHorizontal;
isTouchingCeiling = CollisionSenses.Ceiling;
}
}
public override void Enter()
{
base.Enter();
player.JumpState.ResetAmountOfJumpsLeft();
player.DashState.ResetCanDash();
}
public override void Exit()
{
base.Exit();
}
public override void LogicUpdate()
{
base.LogicUpdate();
xInput = player.InputHandler.NormInputX;
yInput = player.InputHandler.NormInputY;
JumpInput = player.InputHandler.JumpInput;
grabInput = player.InputHandler.GrabInput;
dashInput = player.InputHandler.DashInput;
if (player.InputHandler.AttackInputs[(int)CombatInputs.primary] && !isTouchingCeiling)
{
stateMachine.ChangeState(player.PrimaryAttackState);
}
else if (player.InputHandler.AttackInputs[(int)CombatInputs.secondary] && !isTouchingCeiling)
{
stateMachine.ChangeState(player.SecondaryAttackState);
}
else if (JumpInput && player.JumpState.CanJump())
{
stateMachine.ChangeState(player.JumpState);
}else if (!isGrounded)
{
player.InAirState.StartCoyoteTime();
stateMachine.ChangeState(player.InAirState);
}else if(isTouchingWall && grabInput && isTouchingLedge)
{
stateMachine.ChangeState(player.WallGrabState);
}
else if (dashInput && player.DashState.CheckIfCanDash() && !isTouchingCeiling)
{
stateMachine.ChangeState(player.DashState);
}
}
public override void PhysicsUpdate()
{
base.PhysicsUpdate();
}
}
We started in the PlayerGroundedState for a reason. It is one of our Super States. Meaning if we were to declare the reference to the CollisionSenses core component as protected instead of private, all of its sub-states would have access to this core component.
So let’s consider all of the sub-states to the PlayerGroundedState super state. This includes the:
- PlayerMoveSate
- PlayerIdleState
- PlayerCrouchMoveState
- PlayerCrouchIdleState
- PlayerLandState
These states all require access to the Movement core component. So let us add the following to our PlayerGroundedState.cs file.
protected Movement Movement { get => movement ?? core.GetCoreComponent(ref movement); }
protected Movement movement;
With this change we can then go to each of the sub-states mentioned before and change core.Movement… to just Movement?…
And just like that the sub states have access to the core component again.
However, notice thatย after Movement we added a ‘?‘. This creates ‘?.‘ which is the null conditional operator. This means that any of these methods that exist on the Movement core component will only get called if the Movement variable is not null. So this means that if our reference was not set, we won’t get a bunch of errors from trying to call a function that does not exist. We need this because, even though we are checking to see if it is null in the getter, and then calling GetCoreComponent, we cannot guarantee that we will find the core component. So instead of breaking the game, we simply rely on the warning message that is presented when an GetCoreComponent does not find anything. Also note however that in PlayerCrouchMoveState, there is no ? after Movement inside the function parameter. This is because there we are trying to access a property and not a function and thus cannot be null. This is the same reason why in PlayerGroundedState we wrapped the collision checks in an if statement instead of using ‘?‘.
Movement?.SetVelocityX(playerData.crouchMovementVelocity * Movement.FacingDirection);
So the following steps can be repeated:
- Consider a super state.
- Consider all sub-states that belong to said super state and determine all core components needed.
- Add there references to these core components in the super state.
- Make use of those references in the sub-states.
I won’t be going every single error in this post as that is just overkill, but all the updated files are available in GitHub for you to compare. However, I will list every core component that needs to be added to each super state.
- PlayerAbilityState
- private CollisionSenses
- protected Movement
- Used in:
- PlayerAttackState
- PlayerDashState
- PlayerJumpState
- PlayerWallJumpState
- PlayerTouchingWallState
- private CollisionSenses
- protected Movement
- Used in:
- PlayerWallSlideState
- PlayerWallGrabState
- PlayerWallClimbState
ย
After this, only two states remain: PlayerInAirState and PlayerLedgeClimbState. So we simply need to add the appropriate references there.
Fixing Errors on the Enemies
After fixing all the errors on the player states, we should see the following:
Unfortunately our enemies still make use of an older finite state machine setup and thus do not make use of super-states and sub-states. So we will therefore have to fix the errors on each enemy state individually. Luckily that process remains the same as we have done so far.
Note that the E1_PlayerDetectedState makes use of the Movement core component, so in the PlayerDetectedState use protected instead of private.
After fixing all the errors with the enemy states we should only have about 10 errors left. 9 of them reside in our actual core components and the last one is on our AggressiveWeapon.cs script.
Fixing Errors on the Core Components and Weapon Script
Some of our core components, like the Combat core component, rely on other core components to function. We will treat this the same as with the states.
Let’s consider Combat.cs:
Combat.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Combat : CoreComponent, IDamageable, IKnockbackable
{
[SerializeField] private float maxKnockbackTime = 0.2f;
private bool isKnockbackActive;
private float knockbackStartTime;
public override void LogicUpdate()
{
CheckKnockback();
}
public void Damage(float amount)
{
Debug.Log(core.transform.parent.name + " Damaged!");
core.Stats.DecreaseHealth(amount);
}
public void Knockback(Vector2 angle, float strength, int direction)
{
core.Movement.SetVelocity(strength, angle, direction);
core.Movement.CanSetVelocity = false;
isKnockbackActive = true;
knockbackStartTime = Time.time;
}
private void CheckKnockback()
{
if(isKnockbackActive && core.Movement.CurrentVelocity.y <= 0.01f && (core.CollisionSenses.Ground || Time.time >= knockbackStartTime + maxKnockbackTime))
{
isKnockbackActive = false;
core.Movement.CanSetVelocity = true;
}
}
}
All lines causing errors are highlighted. As we can see, the Combat core component makes use of: CollisionSenses, Movement, and Stats. So let’s therefore declare:
private Stats Stats { get => stats ?? core.GetCoreComponent(ref stats); }
private CollisionSenses CollisionSenses { get => collisionSenses ?? core.GetCoreComponent(ref collisionSenses); }
private Movement Movement { get => movement ?? core.GetCoreComponent(ref movement); }
private Stats stats;
private CollisionSenses collisionSenses;
private Movement movement;
And then simply adjust the lines causing errors to make use of these new variables instead.
The other core component causing issues isย the CollisionSenses core component as it is making use of the Movement core component. So imply repeat the same there. We should then only be left with the last error in the AggressiveWeapon.cs file. Here we are trying to access the facing direction through the Movement core component. So once again, just declare the two variables to store the reference and update its usage in the code.
With that our console should now be squeaky clean. At this point it is a good idea to run the game and ensure everything is still working as expected. Also try disabling a core component game object on the player and running the game to ensure that the warning pops up correctly. You’ll notice that we still get some errors and not just the warning as we were not able to use the null conditional operator ‘?.‘ everywhere. But because we used it in some places it will slim down the error log slightly.
Conclusion
And so I think this is a good place to end the tutorial. We did a whole lot of refactoring and we learned about a few new operators. Hopefully you found the content useful and managed to learn something. As always, if something did not make sense or you have an opinion on how we could do things better, please reach out on discord or in the comment section.
Once again, thank you so much to all of my Patrons who have helped make all of this possible. See you guys in the next article ๐
Ooh look a comment section
Thanks for the great tutorial! I have a question btw, why is it that when we declare Movement and CollisionSenses, we declare them as protected, and private, respectively? Is there a reason why both aren’t private, or both aren’t protected?
Hey! So declaring them as private or protected will depend on where we need to make use of those components. In the case of our player states, for some of them only the super state made use of CollisionSenses so we make it private. But then many of that super states sub-states made use of movement, so we made Movement protected so that they would have access to it. Hope that helps! ๐
Thanks a bunch!!
hey, so it looks like there could be a problem with inAir. When I press the jump button the player jumps and the animation plays but when I land the inAir animation bool is still checked. I downloaded the project and it looks like your project is having the same problem
Hey! Sorry I thought I had pushed the commit with the fix. So in the PlayerInAir state I was accidentally using collisionSenses instead of CollisionSenses in the DoChecks function. So the reference was never being set. I have updated the github link ๐
Awesome
Wow thank you so much for this amazing tutorial series, canโt wait to watch the new video
Great Work Bradent!
You continue to make this process simple and easy to understand. The community appreciates all of your hard work between these written tutorials and the video tutorials as well!
I personally enjoy the written tutorials as they allow me to work at my own pace and compare the code sections if I create an unexpected error more easily!