Determine if the enemy is attacking from the front or the back

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 :raised_hands:

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

2 Likes

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);
1 Like

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 :raised_hands:

1 Like

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

2 Likes

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!

bandicam 2023-02-04 13-23-05-064

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

1 Like

Privacy & Terms