My new Stealth System

Thats for today i will do more tests tommorow just for any case but for today i call it a day, also it will get probably upgraded later when i finish my abilities tutorials and quests and shop tutorials etc , its up for discusions waiting to hear more ( print still exists because its still under construction so de bugging should exist :stuck_out_tongue: , Yes for some reason i print instead of debugg.log.

First the base system of how stealth and perception works it can be upgraded aslo through some abilties passives etc that would come later when i reach and finish the Abilities course :

namespace RPG.Combat
{
    public class StealthSystem : MonoBehaviour
    {

        [SerializeField] GameObject bodyAppear;

       [SerializeField] bool canDetect = false;
        [SerializeField]bool startsHiden = false;

       [SerializeField] bool isStealth; 
        public bool IsStealth()
        {
            return isStealth;
        }
        float currentStealthVallue;
        public float CurrentStealthVallue()
        {
            return currentStealthVallue;
        }
        [SerializeField] int testStealth;
        float activeStealthVallue;
        [SerializeField] int testPerception;        
        BaseStats stats;

        private void Awake()
        {
            stats = GetComponent<BaseStats>(); 
        }

        // Start is called before the first frame update
        void Start()
        {
            if (startsHiden)
            {
                Invoke("ActivateStealth", 0.1f);
            }
        }

     

        // Update is called once per frame
        void Update()
        {
            activeStealthVallue = GetInitialMaxStealth();
        }

       public float GetInitialMaxStealth()
        {
            return stats.GetStat(Stat.Dexterity) + testStealth;
        }

        public float GetInitialMaxPerception()
        {
            return stats.GetStat(Stat.Inteligence) + testPerception;
        }

        public void StealthTrigger()
        {
            if (isStealth)
            {
                DeactivateStealth();
                print("my current stealth is :" + currentStealthVallue);
            }
            else
            {               
                ActivateStealth();
                print("my current stealth is :" + currentStealthVallue);
            }

        }

        public void DeactivateStealth()
        {
            isStealth = false;
            StealthBehavior();
        }

        public void ActivateStealth()
        {
            isStealth = true;
            StealthBehavior();
        }

        void StealthBehavior()
        {
           if(this.gameObject.tag == "Player")
            {
                if (isStealth)
                {
                    currentStealthVallue = activeStealthVallue;
                    return;
                }
                else
                {
                    currentStealthVallue = 0;
                                    
                }
                return;                
            }
            if (isStealth)
            {
                currentStealthVallue = activeStealthVallue;
                bodyAppear.SetActive(false);
                return;
            }
            currentStealthVallue = 0;
            bodyAppear.SetActive(true);
            
            

        }

        public int PerceptionRoll()
        {
            return Random.Range(0, 10);
        }           
        


    }

    

}

Now we want something that would make our enemies able to see us when the time is come :

IEnumerator PerceptionChecks()
        {
            float Detection = this.gameObject.GetComponent<StealthSystem>().GetInitialMaxPerception();
            float Plstealth = player.GetComponent<StealthSystem>().CurrentStealthVallue();
            int stealthFailedAttemps;

           
            tryDetect = true;
            print("player steath = " + Plstealth + " and my perception is =" + Detection );

            for (stealthFailedAttemps = 0; Detection < Plstealth; stealthFailedAttemps++)
            {
                
               
                if(stealth.PerceptionRoll() + stealthFailedAttemps + Detection > Plstealth)
                {
                   
                    break;
                }
                else if(stealth.PerceptionRoll() + stealthFailedAttemps + Detection > Plstealth / 1.5f)
                {
                    transform.LookAt(player.transform.position);                   
                }
                if (!IsAggrevated())
                {
                    break; 
                }
                print(" my roll would be :" + stealth.PerceptionRoll() + " i have failed to see you :" + stealthFailedAttemps);

               Plstealth = player.GetComponent<StealthSystem>().CurrentStealthVallue(); 

                yield return new WaitForSeconds(1.5f);
            }
            if (IsAggrevated())
            {
                print("I SEE YOU");
                AttackBehaviour();
                foundPlayer = true;               
            }
            else
            {
                foundPlayer = false;
                tryDetect = false;
                print("it seems i was imagine things");                
            }

          
            
        }

Also a diffrent aproach during aggrevated :

 private void Update()
        {
            if (health.IsDead()) return;


                      
            if (IsAggrevated() && fighter.CanAttack(player))
            {
                if (foundPlayer)
                {
                    AttackBehaviour();
                    return;
                }            
                    
                
                 if (tryDetect)
                {
                    
                    return;
                }
                StartCoroutine(PerceptionChecks());



            }

Now we wish an approach that would the play able to turn stealth but also a way that would prevent player turn stealth in the middle of a fight the way that i would stop player turn stealth in the middle of the fight is commented and not yet in game.

 if (InteractiWithUI()) return;
            if (health.IsDead())
            {
                if (stealth.IsStealth())
                {
                    stealth.DeactivateStealth();
                }               
                SetCursor(CursorType.None);
                return;
            }

            if (Input.GetKeyDown(KeyCode.C))
            {
                if (stealth.IsStealth())
                {
                    stealth.StealthTrigger();                    
                    return;
                }
                // going add an else if trigger that if play is inCombat wouldnt be able re activate stalth
                else 
                {
                    stealth.StealthTrigger();
                    return;
                }
                
            }

However we want to make an sneak attack it could be extra damage or whatever but attacking should reveal your possiton so its time to implent that :

 void Hit()
        {

            if (target == null) { return; }
            if (stealth.IsStealth())
            {
                print(gameObject.name + " i sneak attack you");
                stealth.DeactivateStealth();
            }
            float attackDamage = IntPower() + DexPower() + StrPower() + stats.GetStat(Stat.BonusDamage);

            if(currentWeapon.value != null)
            {
                currentWeapon.value.OnHit();
            }

            if (currentWeaponConfig.HasProjectile())
            {
                currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, target, gameObject, attackDamage);
            }
            else
            {
                target.TakeDamage(gameObject, attackDamage);
            }
        }

and now if you take damage you must stoped being stealth so its time to make that happens :

   public void TakeDamage(GameObject instigator, float damage)
        {
            float Hplost;
            Hplost = damage - armor;
            if(Hplost < 1)
            {
                Hplost = 1;
            }            
            currentHealth = Mathf.Max(currentHealth - Hplost, 0);
            if (stealth.IsStealth())
            {
                stealth.DeactivateStealth();
            }           
            if (IsDead())
            {
                onDie.Invoke();
                Die();
                AwardExperiance(instigator);
            }
            else
            {
                takeDamage.Invoke(Hplost);
            }
        }

Now the next step is making player able to detect hiden enemies or even objects. For objects thing a little change on the stealth system maybe exist or a new script that inherits from stealthsystem named as hiddenObjects maybe come that would the players perception make reveal them ( turning their stealth off) , also i would try make the raycast not trigger while mouse pass over a stealth enemy or object.

two screenshots about stealth enemies
and appearance when you hit them:

the fireball travels to the enemy ( unseing enemy)

the fireball hits the enemy and the enemy appears :

if you wish to see what happens when i am close at enemies at stealth i can post more etc. Thanks a lot in advance

Rather than pasting in screenshots of code, it’s usually best to paste the code in as text.
Start by putting three backwards apostrophes on its own line (this is not the ’ by the return key, but the ` next to the 1 on your keyboard like this
```
Then on the next line, paste in your code.
At the end,put three more backwards apostrophes on their own line to close out the code block

1 Like

fix it thanks a lot and sorry for the wrong way of post it :slight_smile:

i would also realy love if you could give a quick look and tell me your oppinion.

Also i found that as a solution for the rays i want your oppinon on that because it seems to wrok fine but… i dont know if it could casue a lot of problems in future i show the code :

  void StealthBehavior()
        {
           if(this.gameObject.tag == "Player")
            {
                PlayerStealth();
                return;
            }

            EnviromentStealth(); 
        }

        private void EnviromentStealth()
        {
            if (isStealth)
            {
                currentStealthVallue = activeStealthVallue;
                bodyAppear.SetActive(false);
                gameObject.layer = 2; 
                return;
            }
            gameObject.layer = 0; 
            currentStealthVallue = 0;
            bodyAppear.SetActive(true);           
        }

Also as a change to make objects using the same script i found that way to do it without inheritance etc also i would like your oppinion on that :

 public float GetInitialMaxStealth()
        {
            if( stats != null)
            {
                return stats.GetStat(Stat.Dexterity) + testStealth;
            }
            return testStealth; 
            
        }

        public float GetInitialMaxPerception()
        {
            if (stats != null)
            {
                return stats.GetStat(Stat.Inteligence) + testPerception;
            }
            else return testPerception; 

        }

thanks a lot and sorry for the trouble

Lots to unpack here… :slight_smile:

Changing the GameObject’s layer is a potential solution to the Raycast issue, but it might be better to simply add a check in CombatTarget’s IHandleRaycast

if(TryGetComponent(out StealthSystem stealthSystem) && stealthSystem.IsStealth()) return false;

This is advantageous over setting layers, which you may wish to change later.

I like the coroutine idea, you might include some sort of inverse proximity bonus in addition to the int failed attempts… I would like to point out that stealth.PerceptionRoll() will be different every call, so when it’s first used in the if statements, it will be a different value than when you print it out at the end of the if statements. It would be better to cache this value at the beginning of the int statements. Here’s how I would likely write this:

IEnumerator PerceptionChecks()
        {
            float Detection = this.gameObject.GetComponent<StealthSystem>().GetInitialMaxPerception();
            float Plstealth = player.GetComponent<StealthSystem>().CurrentStealthVallue();
            int stealthFailedAttemps;         
            tryDetect = true;
            Vector3 lastPosition = player.transform.position;
            print("player steath = " + Plstealth + " and my perception is =" + Detection );

            for (stealthFailedAttemps = 0; Detection < Plstealth; stealthFailedAttemps++)
            {
                //Cache perceptionRoll to keep same through method
               float perceptionRoll = stealth.PerceptionRoll();

                //The closer you are to the player, the higher the bonus
                float distanceBonus = chaseDistance - Vector3.Distance(transform.position, player.transform.position);

                //This bonus is based on player's movement.  If a player stands still, there is no bonus
                //But if a player moves, then the enemy gets a bonus to detect based on distance moved
                float movementBonus = Vector3.Distance(lastPosition, player.transform.position);
                lastPosition = player.transform.position;
                
                float effectiveRoll = perceptionRoll + distanceBonus + movementBonus;
                if(effectiveRoll + stealthFailedAttemps + Detection > Plstealth)
                {
                    //Brute force.  If I see you, I'm likely to tell my friends, so stealth is broken
                    player.GetComponent<StealthSystem>().DeactivateStealth(); //
                    break; 
                }
                else if(effectiveRoll + stealthFailedAttemps + Detection > Plstealth / 1.5f)
                {
                    print("Hey, did you guys hear that?");
                    transform.LookAt(player.transform.position);                   
                }
                if (!IsAggrevated())
                {
                    break; 
                }
                print(" my roll would be :" +  + " i have failed to see you :" + stealthFailedAttemps);
                print("My roll was {effectiveRoll} ({perceptionRoll} + {distanceBonus} + {movementBonus}) and I have failed to see you.  {stealthFailedAttempts}");
               Plstealth = player.GetComponent<StealthSystem>().CurrentStealthVallue(); 

                yield return new WaitForSeconds(1.5f);
            }
            if (IsAggrevated())
            {
                print("I SEE YOU");
                AttackBehaviour();
                foundPlayer = true;     
                tryDetect = false; //cleanup state.  It may not cause a bug now, but not resetting it could if you made updates in the future.          
            }
            else
            {
                foundPlayer = false;
                tryDetect = false;
                print("it seems i was imagine things");                
            }

          
            
        }
1 Like

It was really amazing the whole idea about the Extra coroutine stuff. That could also help easily to create some Extra stuff for stealth bonuses for example some full of grass areas could give a bonus to your stealth etc ( balancing reasons mostly ) i will test it when I am back home soon. Also whatever different than changing layers would feel better :joy::joy::joy:, it felt somehow wrong to me and hasty solution.
Also the trydetect thing working as part of coroutine could really help.

Thanks a lot Brian.

Well… everything amazing. There is a small problem with the combat target change it’s change the cursor type giving a meta information that something exists there. So i should implement something i quess or create a custom layer that would be enemy only that could be able ignore raycast while Is stealth while stop ignoring raycast if Istealth is false?

That’s… odd… as InteractWithMovement should have gotten a false result…
Which cursor is returning?
Paste in your InteractWithMovement method from AIController…

well i get the none cursor ( normaly i should get the movement cursor which means it should return true, so the player would move toward his stealth assassin not seing its cursor sudden change. )

   private bool InteractWithMovement()
        {
       
                 
            Vector3 target;
            bool hasHit = RaycastNavMesh(out target);
            if (hasHit)
            {


                
                if (!GetComponent<Mover>().CanMoveTo(target)) return false;
                if (Input.GetMouseButton(0))
                {
                    GetComponent<Mover>().StartMoveAction(target , playerSpeedFraction);
                }
                SetCursor(CursorType.Movement);
                return true;
            }
            return false;
        }
 if (!GetComponent<Mover>().CanMoveTo(target)) return false;

actualy here i sense i can do the change to return true

Screenshot_214

I think I know what’s going on… the RaycastNavMesh is still picking up the character’s capsule collider, which means unless it’s very close to the ground, it’s going to come up can’t move here.
Turns out changing the layer to IgnoreRaycast was the right call after all…

and i am realy afraid of it… I dont know its first on my life playing with layers i dont know if it could have more impact on later phases of game dev.

i have playing with tags before a lot of times change and alliance tag into enemy tag also based on change enable or disable different scirpts on the target. But layers its first time also seing that layers many times interferce with renderings makes me afraid , also ignoreraycast layer could create a problem now that i ve to implant the player perception as long i cant call perception checks based on multiple raycastableobjects. So i must found a different approach. Quick thoughs was going into an like enemy aggrivated thing but findobjects of tags with the tags of objects that could be hidden.

Also Aggrivatenearbyenemies wouldnt work :frowning: its based on raycastablehits, another option is removing the none cursor :stuck_out_tongue:

found a realy temporary solution that i would still create some problems but i thing i found the way little or less

   private bool InteractWithMovement()
        {
       
                 
            Vector3 target;
            bool hasHit = RaycastNavMesh(out target);
            if(TryGetComponent(out StealthSystem stealthSystem) && stealthSystem.IsStealth())
            {
               
                if (Input.GetMouseButton(0))
                {
                    GetComponent<Mover>().StartMoveAction(target , playerSpeedFraction);
                }
                SetCursor(CursorType.Movement);
                return true;
            }
            if (hasHit)
            {


                
                if (!GetComponent<Mover>().CanMoveTo(target)) return false;
                if (Input.GetMouseButton(0))
                {
                    GetComponent<Mover>().StartMoveAction(target , playerSpeedFraction);
                }
                SetCursor(CursorType.Movement);
                return true;
            }
            return false;
        }

Here on that check if i can do that hasHit ignore the raycast ( so i not make the whole thing ignoreraycast) but just ignoretheracast to pass through the game object hit the navmesh

You’re checking the player’s stealthsystem and if that system is stealthed… so I’m not sure how that will avoid the cursor when going over the enemy…

Best option is to create a layer for ground and put the terrain plus anything that the character might walk on, then use that layer in a layermask in the RaycastNavMesh method.

well i will need a bit of help here , i decide that i change layermasks still i create a new layer named as hidden so now stealth would still change layer to 6 ( hidden) while not stealth will revert it back to 0 ( default) . Now i was looking for the layermask thing so that i can exclude layermask 6 only from the playercontroler RaycastMethod ( maybe from RayCastAllSorted as well because the interactwithComponent is first then movement) . The problem is i dont understand well how layermask can exclude only layers 6th , ended up excluding all layers .

 [SerializeField]
        private LayerMask layermask;

i am such an idiot thats all but i was looking rigibody instead of playercontroler …

That’s why it’s easier to go the other way, since what we’re after in RaycastNavMesh is just the ground layer anyways…

Create a layer “Ground”
Assign it to the terrain and anything you might walk over.

In PlayerController, add:

[SerializeField] LayerMask layerMask;

And in the inspector, clear all the check marks except Ground.

In RaycastNavMesh, add a distance and the layerMask

bool hasHit = Physics.Raycast(GetMouseRay(), out hit, 1000, layerMask);
1 Like

Looks like we cross posted on that…

1 Like

Yes works sweet i thing its done now Player perception its easy :

    RaycastHit[] RayCastAllSorted()
        {
            RaycastHit[] hits = Physics.SphereCastAll(GetMouseRay(), raycastRadius, Mathf.Infinity, layermask);
            float[] distances = new float[hits.Length];
            for (int i = 0; i < hits.Length; i++)
            {
                distances[i] = hits[i].distance;
            }
            Array.Sort(distances, hits);

            return hits;

            
        }
 private bool RaycastNavMesh(out Vector3 target)
        {
            target = new Vector3(); 
            
            RaycastHit hit;
            bool hasHit = Physics.Raycast(GetMouseRay(), out hit, Mathf.Infinity, layermask);
            if (!hasHit) return false;

            

            NavMeshHit navMeshHit;
            bool hasCastToNavMesh = NavMesh.SamplePosition(
                hit.point, out navMeshHit, maxNavMeshProjectionDistance, NavMesh.AllAreas);

            if (!hasCastToNavMesh) 
            {
               return false; 
            } 

            target = navMeshHit.position;


            return true;
        }

Thanks a lot and sorry if i put you into trouble.

Changed infinity to 1000 as well makes more sense after all :stuck_out_tongue:

Also i call it a day for now i start doing the most stupid mistakes :slight_smile: thanks brian a lot again i hope to upgrade it even more in the future, also i would realy like an oppinon on the whole system do you believe its flexible easy edditable would you use such on an point click commercial Indie rpg game ?

Player perception is ready , we create 2 differents layermask , the first mask is the mask that works for everything we did until now move or interact with gameobjects etc but will ignore our new layer 6th. while the new mask named as percpetionMask is a new layer (6th) that we named as hidden, its only for our perceptions thing

 [SerializeField] LayerMask layermask;
 [SerializeField] LayerMask perceptionMask;

Now as far as our code, we create a new foreach loop that will collide with our gameObjects inside our perceptionDistance, but will ignore all raycasts that arent at layermask 6th , so all hidden objects must be hit so thats why we create a foreach loop multiple hidden objects may exist we must found them all and make them vissible again so we change their layer so they can be hit with our others raycast methods. Now you see a new thing names as dices after we see that script.

 private void Detect()
        {
            RaycastHit[] hits = Physics.SphereCastAll(transform.position, perceptionDistance, Vector3.up, 0, perceptionMask);
            foreach (RaycastHit hit in hits)
            {
                StealthSystem stealthObject = hit.collider.GetComponent<StealthSystem>();

                float objectStealth = stealthObject.CurrentStealthVallue();
                float detectVallue = this.stealth.GetInitialMaxPerception();
                float distanceBonus = perceptionDistance - Vector3.Distance(transform.position, stealthObject.transform.position);

                print("my detection is" + detectVallue + " while your stealth was " + objectStealth);
                if (objectStealth + dices.TenSideDice(1)  < detectVallue + dices.TenSideDice(1) + distanceBonus)
                {                  
                    stealthObject.DeactivateStealth();
                    return;
                }
                print("nothing here"); 
            }
        }

so lets see our new system named as dices its good if you wish to add little randomize dnd like to your game but it can be ignored entirely as well. Or even change the way is being used. Lets see our dice script :

public class DiceRoll : MonoBehaviour
{
    int totalDice = 0; 

    public int FourSideDice(int numberOfRolls)
    {
        int i;
       int totalRolls = 0; 
        for ( i = 0; i < numberOfRolls; i++)
        {
            totalDice = (Random.Range(1, 4));           
            totalRolls = totalRolls +  totalDice;          
        }
        return totalRolls;  
    }
    public int SixSideDice(int numberOfRolls)
    {
        int i;
        int sumUpOfRolls = 0;
        for (i = 0; i < numberOfRolls; i++)
        {
            totalDice = (Random.Range(1, 6));
            sumUpOfRolls = sumUpOfRolls + totalDice;
        }
        return sumUpOfRolls;
    }
    public int EightSideDice(int numberOfRolls)
    {
        int i;
        int sumUpOfRolls = 0;
        for (i = 0; i < numberOfRolls; i++)
        {
            totalDice = (Random.Range(1, 8));
            sumUpOfRolls = sumUpOfRolls + totalDice;
        }
        return sumUpOfRolls;
    }
    public int TenSideDice(int numberOfRolls)
    {
        int i;
       int sumUpOfRolls = 0;
        for (i = 0; i < numberOfRolls; i++)
        {
            totalDice = (Random.Range(1, 10));           
            sumUpOfRolls = sumUpOfRolls + totalDice;            
        }
        return sumUpOfRolls;
        
    }

    public int PercentageDice()
    {
       return (Random.Range(1, 100));
    }

A thing that i would like to change is that i dont want to attach that script to any Gameobjects or attach to a signle gameObject lets name it gameManager so it will be accesable as a generic script, also instead of totalDice number its better i thing click a temporary dice in each dice.
Also @ Brian_Trotter thats the aproach i choose i would realy like to tell me your oppinion on this

Actually, this class has all the signs of a perfect static class.
Static classes don’t need to be attached to a GameObject or possessed/instantiated by another script, they simply exist. Here’s a rewrite of the script that also makes dice script roll the size and number you wish. That makes it more of a D.R.Y. solution (Don’t Repeat Yourself) rather than a W.E.T. solution (We Enjoy Typing; Write Everthing Twice, you get the idea).

public static class DiceRoller
{
    public static int RollDice(int numberOfSides, int numberOfRolls);
    {
         int result = 0;
         for(int i=0;i<numberOfRolls;i++)
         {
             result += Random.Range(1, numberOfSides+1);
         }
         return result;
    }

    public static int RollPercentage()
    {
         return Random.Range(1,101);
     }
}

These methods can be called by simply calling DiceRoller.RollDice(numberOfSides, numberOfRolls), so if you want to roll 3d6, you’ll call DiceRoller.RollDice(6,3);

A technical note on die rolls… if you’re rolling a four sided die, you actually need to take Random.Range(1,5), not 1,4. This is because Random.Range’s results for integers will be between the 1st parameter and the last-1, so 1-4 would yield 1,2 or 3. As you can see, I modified the Random roll to roll between 1 and numberOfSides+1 to compensate. (You could also use Random.Range(0,numberOfSides)+1
This behavior is actually quite handy for things like picking a random element from a list, because Random.Range(0, myList.Count) is guaranteed to return a valid result (because arrays and lists are 0 based indexes.

1 Like

Amazing thanks a lot Brian

Privacy & Terms