How to create an aim system like project zomboid?

So I’m working in a top down shooter in Unity3d, and my aim system is exactly like in a normal first person shooter, Im using raycast to check if theres a collision in the direction I pointed with the mouse. Pretty simple and normal stuff and its working properly, but because the camera is far from the zombies and me, and the camera also have an angle, aiming in the conventional way seems not intuitive, against a herd of zombies its ok, but I’m already experiencing difficulties when targeting a single zombie.

Project Zomboid addresses this problem very well, for those who didnt played instead of checking if a point collided with one zombie it instead target automatically the closest zombie that you are facing, so you dont really need to aim just look at the zombie and shoot (in the game is more complex, like u have chances to miss the shots depending on your level and the distance between you and the zombie, and weapons like shotgun can target more than 1 zombie, but lets start more simple).

yeah after this long explanation any ideas to how implement this in code? I tried to search how the team behind PZ made this or how to do in unity and find nothing… If u have some functions, documentation, code, videos explaining this I would appreciate.

The logic im having is to have a field of vision that is able to get all the zombies inside it, check which one is closer and target them, and when the player shoots is automatically does damage.

PZ

The way I implemented using raycasting


I would start with considering all action to happen in an invisible plane at the height of the player’s gun.

Then you raycast from the mouse into the scene and find the spot on this invisible plane and that is where you aim.

There’s a few ways you could do this. I’ve used all of them at some point and they all have their ups and downs.

The easiest would be to simply create a geometric model in something like blender or even using ProBuilder in Unity that represents the cone for the Area you want to check. Attach it to the player so that it is facing the same direction they are aiming. Then you can use that model as an invisible trigger. What’s great about this is that you can even use different shaped models for different weapons. When targets are detected inside of the trigger and you’re intending to aim and shoot at one of them you’ll have to iterate through all of them and decide which one is the true target based on some heuristic that you deem best. I usually go with selecting the ones most directly in the center line of the shape and then the closest from that group. If no targets are within the cone then you can either assume that the shot always misses or just do a basic raycast directly in line with the player’s view.

Another similar way would be to use some kind of box check at the time of firing and do something similar to above, perhaps taking angles for cone of vision into account. Kirk’s method is also pretty good and probably more performant though that would likely only matter if you had an awful lot of shooting and zombies going on (which might be the case, zombies after all!)

1 Like

Really like both ideas, ill try to implement soon, if I got some problems I will post here, for now thank you guys!

Oop, I failed to read the above part… exactly what @Sluggy1 says, or perhaps what @zulo3d says… either way.

The sticky bits I see are what to do when you are looking at zero enemies (so it shoots directly to center of cone), but then an enemy pops into the corner. Do you snap to him instantly? Slew to him slowly? Does the difference between your heading and your shooting affect accuracy? Etc.

I would start with every frame snapping instantly (or slewing VERY rapidly) to the nearest target, because it will feel like a very responsive player avatar.

Lots of fun play surfaces here to mess with, especially with different weapons, defined perhaps in different ScriptablObject instances.

Yeah this method seems very fun to work on, I dont remember if this happen in PZ but I think when the zombie show up in the corner you can already shoot it, but we are probably going to missing it, like 90% chance, so you need to wait a time or aim better, I saw @zulo3d post and the Physics.OverlapSphere method seems very useful in this situation, I was already watching a video about it XD thanks man

1 Like

Ok hi again guys, I already got it ^^. So the suggestion by @zulo3d is what I used, the idea is to use Physics.OverlapSphere to create a circle around the player and set a maximum angle to get the enemies.
I used this video to make the calculations and the debug to be able to see what is happening, but needed to change a lot, keeping only the basis of logic.

First the code gets an array containing all colliders it gets inside of the circle. Then I loop through each one of the colliders and check if the position of the collider is inside the angle that I set, then it creates a raycast between the player and the monster, checking if theres a wall.

if theres no wall then I put the transform component inside a list of transform. In the end of the iteration I return the list

public List<Transform> FieldOfViewCheck()
{
    Collider[] rangeChecks = Physics.OverlapSphere(player.position, radius, targetMask);

    //The list containing the transforms of monster I can shoot
    List<Transform> monsters = new List<Transform>();

    //Did I find any monster
    if (rangeChecks.Length != 0)
    {
        for(int i = 0; i < rangeChecks.Length; i++)
        {
            //Take it transform
            Transform target = rangeChecks[i].transform;

            Vector3 directionToTarget = (target.position - player.position).normalized;

            //If the angle between my vector3 Pos and the vector3 of the monster is less than half of the angle I defined in the inspector (So we're checking if the difference still inside the boundaries that  I set)
            if (Vector3.Angle(player.forward, directionToTarget) < angle / 2)
            {
                float distanceToTarget = Vector3.Distance(player.position, target.position);

                //Instead of checking for a monster I'm checking for a wall, If I dont see a wall then we are seing the monster
                if (!Physics.Raycast(player.position, directionToTarget, distanceToTarget, obstructionMask))
                    monsters.Add(target);
            }
        }
    }
    return monsters;
}

at the gun script I do a loop through all the transforms and check one by one which one is closer, after this loop I create a raycast between the gun and the zombie, and if the raycast hits we use the Get.Component<>() to do damage in the zombie.

    public void Shoot()
    {
        RaycastHit hit;

            List<Transform> monsters = fieldOfView.FieldOfViewCheck();
        if (monsters.Count != 0)
        {
            float distanceToTarget;
            float leastDistanceToTarget = fieldOfView.radius;

            Transform monsterTarget = monsters[0].transform;

            //Loop through all the monster found in fieldOfViewCheck()
            for (int i = 0; i < monsters.Count; i++)
            {

                //Do the distance between the gun and the monster
                distanceToTarget = Vector3.Distance(firePoint.position, monsters[i].position);

                //if the distance is less than the distance of the other zombie (in the first iteration it need to be less    than the radius)
                if (distanceToTarget < leastDistanceToTarget)
                {
                    leastDistanceToTarget = distanceToTarget;
                    monsterTarget = monsters[i];
                }
            }

            Vector3 directionToMonster = (monsterTarget.position - firePoint.position).normalized;
           

            var raycast = Physics.Raycast(firePoint.position, directionToMonster, out hit, leastDistanceToTarget, whatIsMonster);

            if (raycast)
            {

                Monster monster = hit.transform.GetComponent<Monster>();
                if (monster != null)
                {
                    monster.TakeDamage(damage);
                }
            }
        }

     
    }

As u read the code checks with an sphere, loop through all the components inside the range checking for a wall using raycast, then loop again check every distance between the player and the zombie, and after all of that it gets the zombie component. So if u guys have any idea to make this more fast I would appreciate.

Anyway I made this fairly fast because comp-3 video, but I still need to be able to check for multiple targets for some type of weapons, like a double barrel.

j26u3q

I already made this reusable, with a pistol and a sniper, each one having different damage, range, and angle. By theres more room for improvement, for now I need to have a way to change guns without the need to change in the inspector, and a way to get the player transform too (using scriptable objects is worth? i did this with the zombies)

I might do a video about this.

2 Likes

It seems pretty good to me. There’s some minor math tricks you could do with Atan for faster angles, and using squared distance check instead of distance checks. But honestly that’s falling into the realm of micro optimization at this point. Try it out, blast some zombies, and punch the numbers way up and profile it if you really want to know where the bottlenecks are. In my experience it usually ends up being the physics engine or the animation system that kill me first.

2 Likes

Your methods could be combined into a single Shoot method. And you can do the SphereOverlap ahead of the player so then you don’t have to waste time checking the zombies that are behind the player.

    void Shoot()
    {
        Transform zombie=null;
        foreach (Collider c in Physics.OverlapSphere(transform.position+transform.forward*50,100))
            if (Vector3.Dot(transform.forward,c.transform.position-transform.position)>0.7f) // within our field of view?
                if (!zombie || Vector3.Distance(transform.position,c.transform.position)<Vector3.Distance(transform.position,zombie.position))
                    if (Physics.Linecast(transform.position,c.transform.position,out RaycastHit hit)) 
                        if (hit.collider==c)
                            zombie=c.transform; // select this zombie
        if (zombie)
            zombie.GetComponent<Monster>().TakeDamage(damage);
    }