Would love to see what you come up with, If anything is related to refactoring so it can be more readable would be amazing, you know how it goes so the fewer lines the better
OK, So I tested my refactor and it works as I expected. Perhaps you didn’t call the SetInvulnerable()
. My function using the Vector3.Dot
does not set it. You have to do that based on the result.
However, there is a far bigger issue here. All this is ok for a single enemy, but as soon as you have multiple enemies in your list (i.e. within your targeting range) it all falls apart. This is because any enemy that is outside the coverage angle will unset the invulnerability and all hits will land because you are no longer invulnerable.
Consider this
The ‘enemy minding his own business’ is within the targeting range and therefore in your list of enemies but because he is not in the coverage area he will unset IsInvulnerable
and the ‘attacking enemy’ will hit regardless of the fact that he is within the coverage area
Does this look familiar?
What I propose is to change how blocking works in general by removing invulnerability from health and only determining if damage should be applied at the moment of damage dealing. (Edit Do not remove invulnerability from health if you have a ‘dodge’ state)
What I did was to create a DamageReceiver
component. When WeaponDamage
wants to do damage, it will do it through here instead of directly updating Health
. This new DamageReceiver
will be responsible for taking possible damage and determining if the target (player or enemy) should receive this damage. If the target is blocking and the attacker is within the coverage angle, the damage goes away. Otherwise it’s damage as usual.
DamageReceiver.cs
public class DamageReceiver : MonoBehaviour
{
[SerializeField] Health health;
[SerializeField] float coverageAngle = 90f;
private bool isBlocking = false;
public void SetIsBlocking(bool state)
{
isBlocking = state;
}
public void DealDamage(Transform attacker, int damageAmount)
{
if (isBlocking && AttackerInCoverage(attacker)) return;
health.DealDamage(damageAmount);
}
private bool AttackerInCoverage(Transform other)
{
var requiredValue = Mathf.Cos(coverageAngle * Mathf.Deg2Rad);
var directionToAttacker = (other.position - transform.position).normalized;
var dotProduct = Vector3.Dot(transform.forward, directionToAttacker);
return dotProduct >= requiredValue;
}
}
This goes on the player (and enemies) and are added to the StateMachine
PlayerStateMachine Updates
public class PlayerStateMachine : StateMachine
{
// .. snip ...
[field: SerializeField] public DamageReceiver DamageReceiver { get; private set; }
// .. snip ...
}
And the BlockingState
would just set IsBlocking
in the Enter
and Exit
methods. No need to do anything more here (except what was already there from the course, minus Health.Setinvulnerable()
)
PlayerBlockingState Updates
public class PlayerBlockingState : PlayerBaseState
{
// .. snip ...
public override void Enter()
{
_stateMachine.DamageReceiver.SetIsBlocking(true);
}
public override void Exit()
{
_stateMachine.DamageReceiver.SetIsBlocking(false);
}
// .. snip ...
}
WeaponDamage
also needs a little update because we want to do damage through the DamageReceiver
now. We remove the part where we get the Health
from other
and instead get the DamageReceiver
. We also pass the transform to the DamageReceiver
so that it can determine if this attacker is within the coverage angle.
WeaponDamage Updates
private void OnTriggerEnter(Collider other)
{
// .. snip ...
if (other.TryGetComponent<DamageReceiver>(out var damageReceiver))
{
damageReceiver.DealDamage(myCollider.transform, damage);
}
// .. snip ...
}
Oh, I also removed all traces of isInvulnerable
from Health
. I’m no longer using that. (Edit I see this is used in the ‘dodge’ state which I haven’t implemented. Leave it there, it should be fine. Perhaps I’d look into it at some point)
Here is a quick video of the result
This! The content of this post should be part of a future course. So far so good! Everything’s working properly.
It looks like this method of blocking should also cover projectiles, if I’m not mistaken.
As an aside, check out the funny bug that happens when I jump on an enemy 30 seconds in.
As another aside, enemies can hurt each other. I’ve been trying to solve this issue (it seems easy, doing roughly the same thing I do with the character controller, but with an enemy tag to avoid damage, yes?), but no luck so far. Nuts and bolts of Unity coding haven’t sank in with me quite yet.
float angle = Mathf.Abs(Vector3.SignedAngle(stateMachine.transform.forward, playerDirection);
This is amazing. Thank you for taking the time to refactor and improve the code!! This would help a lot to anyone looking for a solution like this that is essential when making a type of game like this
Yes, it should work for projectiles because it is no longer dependent on enemies being within your targeting range.
A tag should work as a simple solution. In the WeaponDamage
script you could check if the other
tag is the same as this
tag and if they match ignore everything (or set damage to 0 so the ForceReceiver
still does its thing but with no damage).
// WeaponDamage.cs
private void OnTriggerEnter(Collider other)
{
if (other == myCollider) return;
if (other.CompareTag(gameObject.tag)) return; // Check if we're friends
if (collidedWith.Contains(other)) return;
//... snip ...
}
This is a simple ‘faction system’ because you can now tag enemies with different tags to indicate their ‘factions’, i.e.
RedEnemy
BlueEnemy
GreenEnemy
and with this, RedEnemy
will not hit other RedEnemy
's but it will hit BlueEnemy
and GreenEnemy
(and the player)
For more flexible ‘factions’, you could add a Faction
component and check that instead. What’s different is that a component will allow some logic like having allied factions.
// Faction.cs
public class Faction : MonoBehaviour
{
[SerializeField] string factionName;
private List<string> alliedFactions = new List<string>();
public bool IsOwnOrAlliedFaction(Faction faction)
=> IsOwnOrAlliedFaction(faction.factionName);
public bool IsOwnOrAlliedFaction(string faction)
=> factionName == faction || alliedFactions.Contains(faction);
public void AddAlliedFaction(string faction)
{
if (alliedFactions.Contains(faction)) return;
alliedFactions.Add(faction);
}
public void RemoveAlliedFaction(string faction) => alliedFactions.Remove(faction);
}
// WeaponDamage.cs
[SerializeField] Faction myFaction;
private void OnTriggerEnter(Collider other)
{
if (other == myCollider) return;
if (collidedWith.Contains(other)) return;
if (other.TryGetComponent<Faction>(out var otherFaction)
&& myFaction.IsOwnOrAlliedFaction(otherFaction))
return;
//... snip ...
}
This will then ignore damage against matching or allied factions
Hi! Back after a couple days break. First of all, thank you so much for putting the time into mapping out not only the answer to my question, but a whole faction system as well. Amazing!
I added your new line if (other.CompareTag(gameObject.tag)) return; // Check if we're friends
At first it didn’t work. Then I thought to change the tags of all enemies’ WeaponLogic from Untagged to Enemy, and all of a sudden it worked!
To experiment, I also tagged the Player’s WeaponLogic as Player and commented out the if (other == myCollider) { return; }
line in WeaponDamage script. So far nothing is different (which is to say Enemies still damage players and not other enemies, and the player damages enemies as normal).
Do you think there could be any case where tagging my Enemy and Player’s WeaponLogic as such could backfire down the line?
Should be fine