Merging the Third Person Controller Course with the RPG Course

I’m patient, trust me I am as patient as I can get - I just hope that the final audience doesn’t go “it’s low poly graphics, we don’t want that” and completely give up on my game. I want this to be fun :slight_smile:

Until one of them returns, I’ll go improve my construction system or something. This one is another occasional nightmare to deal with, but I’m slowly getting the hang of it

Don’t let me read a statement like that when I am drinking a soda! Now help me clean up this monitor!

I’d like to see your Projectile’s OnTriggerEnter, and your Fighter’s TryHit() methods…

1 Like

oh come on though… you know I bring up some great ideas :stuck_out_tongue:

sure, here you go:

// New Code, for Third Person projectiles:
        private void OnTriggerEnter(Collider other)
        {
            // This function tells the projectile holding this script what to do
            // when it hits something, depending on whether it's a target, or just 
            // something in the way
            
            
            if (other.gameObject == instigator) return;
            if (other.TryGetComponent(out Health health)) health.TakeDamage(instigator, damage, instigator.GetComponent<Fighter>().GetCurrentWeaponConfig().GetSkill());
            if (other.TryGetComponent(out ForceReceiver forceReceiver)) forceReceiver.AddForce(transform.forward * damage * 0.5f, true); // multiplied by 0.5f so as to neutralize the knockback impact

            speed = 0;

            onHit.Invoke();

            if (hitEffect != null) Instantiate(hitEffect, transform.position, transform.rotation);

            foreach (GameObject toDestroy in destroyOnHit) Destroy(toDestroy);

            Destroy(gameObject, lifeAfterImpact);

            Debug.Log($"Instigator = {instigator.name}");
            Debug.Log($"Collider = {other.name}");
            Debug.Log($"Damage Dealt = {damage}");

        }

voila:

void TryHit(int slot)
    {

        // if no current attack (or follow up) exists, return null:
        if (currentAttack == null) return;

        // radius of the damage of the weapon:
        float damageRadius = 0.5f;

        // To trigger this animation, the event in the animation takes the 1 and 2 values in 'Int' slot
        Vector3 transformPoint;
        switch (slot) 
        {
            case 0: transformPoint = currentWeapon.value.DamagePoint;   // weapon damage
            damageRadius = currentWeapon.value.DamageRadius;
            break;
            case 1: transformPoint = rightHandTransform.position;   // for right-handed cases
            break;
            case 2: transformPoint = leftHandTransform.position;    // for left-handed cases
            break;
            // case 3: for right foot, but after you got the animation intact and the transform setup
            // case 4: for left foot. Again, after the animation is intact and the transform is setup
            default: transformPoint = rightHandTransform.position;  // for cases when things make no sense
            break;
        }
        Debug.Log($"Attacking with slot {slot}, position {transformPoint}");

            foreach (Collider other in Physics.OverlapSphere(transformPoint, damageRadius)) 
            {
                if (other.gameObject == gameObject) continue;   // don't hit yourself

                if (other.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead()) 
                {
                    float damage = GetDamage();
                    damage *= currentAttack.DamageModifier;

                    // player/enemy
                    if (other.TryGetComponent(out BaseStats otherBaseStats))
                    {
                        float defence;
                        // if its the player (the only in-game character with a skillStore)
                        if (other.TryGetComponent(out SkillStore skillStore))
                        {
                            defence = otherBaseStats.GetStatBySpecifiedLevel(Stat.Defence, skillStore.GetSkillLevel(Skill.Defence));
                        }
                        // the enemy
                        else defence = otherBaseStats.GetStat(Stat.Defence);
                        damage /= 1 + defence/damage;
                        // damage *= UnityEngine.Random.Range(0f, 1.25f);
                        // if (UnityEngine.Random.Range(0,100) > 95) damage *= 2.0f;
                    }

                    if (other.tag == "Player") // (blocking state) or you can try get the SkillStore, either works...!
                    {
                        bool inVulnerable = other.GetComponent<Health>().GetInvulnerable();
                        if (inVulnerable) return; // add more information in here, to accept up to 50% original damage (use Random.Range for that), and impact state for example (the better the shield, the less likely it is to allow damage through)
                    }
                    
                    if (otherHealth.IsDead()) return;   // can't hit a dead enemy
                    otherHealth.TakeDamage(gameObject, damage, currentWeaponConfig.GetSkill());
                    TryApplyHitForce(other, transform.position);
                }
            }
        }

and ‘Shoot()’, if you need it:

    // Called on the ranged animation event lines:
    void Shoot() {

        // In very simple terms, this function is the 'Hit()' function for 
        // Ranged (Projectile) Weapons, to be called in 'Projectile.cs' (for simplicity in naming's sake)
        // Hit();

        // RPG to Third Person Conversion changes are what the rest of this function is all about:
        if (!currentWeaponConfig.HasProjectile()) return;

        if (TryGetComponent(out ITargetProvider targetProvider))
        {
            float damage = GetDamage();
            GameObject targetObject = targetProvider.GetTarget();

            if (targetObject != null)
            {
                // use the 'LaunchProjectile' function with 5 arguments (as we do have a target)
                currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, targetObject.GetComponent<Health>(), gameObject, damage);
                }
            else
            {
                // use the 'LaunchProjectile' function with 4 arguments (as we don't have a target)
                currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, gameObject, damage);
            }

                // If you have enough ammo, fire and use the ammo
                if (currentWeaponConfig.GetAmmunitionItem())
                {
                    GetComponent<Quiver>().SpendAmmo(1);
                }
            }
        }

Side question, how do I move my project safely to another drive on my computer? As we speak, my SSD is running low on space and performance is becoming absurdly slow (I’m not talking about Unity packages, I’m talking about the project itself, literally)

Just copy the whole thing to the other drive.
Then go into the Unity Hub and locate the project by Adding From Disk (and remove the old project from the list).

Ok, looking at your scripts, I’m wondering if you don’t have an extra collider on your Player…
The CharacterController itself is a collider, and if you have an additional collider, then things like OnTriggerEnter() and our results from the OverlapSphere will yield TWO results on the same GameObject, so two hits.

yup… that was it. I had a Capsule Collider for the animal by Malbers and completely forgot that thing even exists (his system won’t support Character Controllers, and it’s driving me nuts)

I’ll start thinking of how to engineer a solution for that (just having a RangeFinder may work for the time being, but on the long run this will be challenging, especially when these animals go into combat mode or what not (I won’t ask for much help with this. This is my territory, I’ll engineer a solution for it on my own :slight_smile:))

If only my laptop would just pick itself up (like I do everyday… :stuck_out_tongue:) and speed up a little bit… (although if I’m 100% honest, you come up with solutions when I’m like… I tried everything I can think of, but all failed… thank you for being there :smiley:)

The simplest solution I can think of, is when we get into Combat mode, just deactivate that Capsule Collider

Can I delete the library folder when doing that? That alone is 17 Gigabytes… :confused: (don’t ask me what’s going on here :sweat_smile:)

We may be able to “engineer” a patch for this since in this case you need the collider…
In TryHit()

void TryHit(int slot)
    {

        // if no current attack (or follow up) exists, return null:
        if (currentAttack == null) return;

        // radius of the damage of the weapon:
        float damageRadius = 0.5f;

        // To trigger this animation, the event in the animation takes the 1 and 2 values in 'Int' slot
        Vector3 transformPoint;
        switch (slot) 
        {
            case 0: transformPoint = currentWeapon.value.DamagePoint;   // weapon damage
            damageRadius = currentWeapon.value.DamageRadius;
            break;
            case 1: transformPoint = rightHandTransform.position;   // for right-handed cases
            break;
            case 2: transformPoint = leftHandTransform.position;    // for left-handed cases
            break;
            // case 3: for right foot, but after you got the animation intact and the transform setup
            // case 4: for left foot. Again, after the animation is intact and the transform is setup
            default: transformPoint = rightHandTransform.position;  // for cases when things make no sense
            break;
        }
        Debug.Log($"Attacking with slot {slot}, position {transformPoint}");
        List<Health> alreadyHit = new List<Health>();
            foreach (Collider other in Physics.OverlapSphere(transformPoint, damageRadius)) 
            {
                if (other.gameObject == gameObject) continue;   // don't hit yourself

                if (other.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead()) 
                {
                    if(alreadyHit.Contains(otherHealth) continue;
                    alreadyHit.Add(otherHealth);
                    float damage = GetDamage();
                    damage *= currentAttack.DamageModifier;

                    // player/enemy
                    if (other.TryGetComponent(out BaseStats otherBaseStats))
                    {
                        float defence;
                        // if its the player (the only in-game character with a skillStore)
                        if (other.TryGetComponent(out SkillStore skillStore))
                        {
                            defence = otherBaseStats.GetStatBySpecifiedLevel(Stat.Defence, skillStore.GetSkillLevel(Skill.Defence));
                        }
                        // the enemy
                        else defence = otherBaseStats.GetStat(Stat.Defence);
                        damage /= 1 + defence/damage;
                        // damage *= UnityEngine.Random.Range(0f, 1.25f);
                        // if (UnityEngine.Random.Range(0,100) > 95) damage *= 2.0f;
                    }

                    if (other.tag == "Player") // (blocking state) or you can try get the SkillStore, either works...!
                    {
                        bool inVulnerable = other.GetComponent<Health>().GetInvulnerable();
                        if (inVulnerable) return; // add more information in here, to accept up to 50% original damage (use Random.Range for that), and impact state for example (the better the shield, the less likely it is to allow damage through)
                    }
                    
                    if (otherHealth.IsDead()) return;   // can't hit a dead enemy
                    otherHealth.TakeDamage(gameObject, damage, currentWeaponConfig.GetSkill());
                    TryApplyHitForce(other, transform.position);
                }
            }
        }

and in Projectile.cs

//outside of any method
List<Health> alreadyHit = new List<Health>();

then within OnTriggerEnter

            if (other.TryGetComponent(out Health health) && !alreadyHit.Contains(health)) 
            {
                 alreadyHit.Add(health);
                 health.TakeDamage(instigator, damage, instigator.GetComponent<Fighter>().GetCurrentWeaponConfig().GetSkill());
           }
            if (other.TryGetComponent(out ForceReceiver forceReceiver)) forceReceiver.AddForce(transform.forward * damage * 0.5f, true); // multiplied by 0.5f so as to neutralize the knockback impact

Yes, in fact, I strongly recommend that. The Editor will rebuild the Library folder when you run the game.

will give this a go and keep you updated (remember when I said my laptop is absurdly slow?)

Edit: seems to be working fine, but there’s a HUGE Drop in Frame Rates that makes testing this really hard. I’ll update you fully about it probably in a few days, since now the laptop has to go through a full scan all over again…

Once done, I’ll transfer the files to another drive so I can get this thing done faster

Edit 2: Brian, can you please elaborate more on how exactly did your modification to my code help clean up the Capsule Collider issue? I’m reading it over and over and over again multiple times, but I’m genuinely confused on what’s going on here… (Laptop is a tiny bit faster, but the fans are loud. I’ll still be unable to work on this for a while unfortunately). I need to understand this so when I get to the point of seriously implementing animals (this may take forever, because MalberS takes a long time to respond sometimes…!), I know what is and what is not possible down the line (and banking later down the line… :stuck_out_tongue: - OK I’ll be quiet)

On a serious note, for the bank (because I’m Bahaa… I always have something different about me that’ll get me suspended soon… :sweat_smile:) - I just want to highlight this comment (now I’ll be quiet… :stuck_out_tongue:) - I’ll give it a go when the export is done, regardless :slight_smile:

I think I see what he is doing. He has added a list of health components that are already hit. When the player takes a hit, the first collider hit will register damage to the player’s health. Then when the second collider triggers, the players health is already on the list so no more damage is applied. That’s a pretty cool solution.

I still have 2 questions though:

  1. From the ‘TryHit’, in the second if statement in the foreach loop, where does this line continue to… to adding the hit to the list, calculating the damage, or…? (I know it’s a basic question, but I can forget things sometimes :sweat_smile:)

for this one, and the previous one as well, how in the world does the code know that we have reached the limit, so now we can ignore anymore hit inputs?

honestly, agreed on that one. Better than toggling on and off the colliders, and then you just don’t know who is working where. Simple and direct, just how I like the code to be :stuck_out_tongue:

  1. The continue takes it back to the foreach loop to check if more colliders got hit.
  2. The if statement in question 1 guarantees only one hit because the player’s health component will always be on the list after the first damage is applied.

so it’s more like a break here…?!

I read the code above again. The if statement is pretty self-explanatory :sweat_smile:

Break ends the foreach loop. Continue goes back and checks the next thing in the list of colliders.

so now it checks the next if statement below it, skipping this part?

On a side note, we need to reduce that projectile knockback equation… The damage in my game racks up quickly, and this one is quickly sending enemies to space :stuck_out_tongue:

The continue makes it skip the entire rest of the body of code inside the foreach loop for the collider currently being checked. Then it goes back to the foreach and checks the next collider. All of the colliders after the first will hit the continue and skip all of the rest of the code in the block. This is what stops the damage from being added a second time. You could have hundreds of colliders (please don’t :rofl:) and damage would only get applied on the first one.

like I said, almost like a break… your explanation is a lot more accurate though, so that’s good

GPU Instancer can take care of that, but my hardware will break its fast really early if I try that :stuck_out_tongue:

Almost, as in nothing really like a break, yes.

foreach(Thing thing in things) //1
{
    if(thing.isNotValid) continue; //returns to 1, checks the next thing in the list of things
    if(Random.Range(0,100) break; //aborts the loop altogether, no other things are considered
    Debug.Log($"{thing} considered");
}
//2

In terms of how the collider trap works, consider the WeaponDamage from the Third Person Course. A list of colliders AlreadyHit() is maintained which resets each time the WeaponDamage is enabled. If a collider is in the list, we’ve already hit it, and we ignore it. If it’s NOT in the list, then we add it to the list as we act on it and damage the character.

fair enough…

Another question on the side. Is there a way to disable some of the buttons on the Action Map in specific states? For example, if my player is in water (now he’s in ‘PlayerSwimmingState.cs’), and he wants to ride a boat, I want him to ride the boat via proximity (I managed to get him to do that already) instead of pressing a button, which is what he does on land (for my scripts at least)

I ask for this because riding a boat from water (I’m diving into my own territory again :confused:) introduces a ton of bugs I want to eliminate, due to my weird solution of getting a player to drive a boat

Usually with the observer pattern…
Perhaps add an event to the Blackboard class

public event System.Action<string> OnBlackboardKeyChanged;

And then in the indexer’s set{} clause, invoke the OnBlackboardKeyChanged;

        public JToken this[string index]
        {
            get
            {
                return blackBoard.TryGetValue(index, out JToken value) ? value : new JValue("");
            }
            set
            {
                blackBoard[index] = value;
                OnBlackboardKey?.Invoke(index);
            }
        }

This means you’ll probably need to add a Blackboard to the Player, and then things that would impede the button would be set to false or enable it to true.

OK I’m a little confused on what exactly I should be doing here… can you please elaborate a little further? Should I add a function in the Swimming State for example? Just a wild guess…

So far, I added what you said I should add above, but I also went to ‘PlayerStateMachine.cs’ and added the following:

namespace RPG.States.Enemies;

        // TEST
        public Blackboard Blackboard = new Blackboard();

what’s next…?

Something else as food for thought. How do we modify the current code to randomly access an animation in a Blend Tree? So for example, if I have an enemy and I want him to access random animations (maybe I have 5 death animations, and I want to access one at random as an example, under a Blend Tree), how would we do that? Just to make combat a little more exciting… :slight_smile:

Privacy & Terms