Sunday, November 3, 2019

Unity : FPS Microgame Analysis -2-


Class: ProjectileBase

As you can see, ProjectileStandard has a member of ProjectileBase. ProjectileBase has a UnityAction variable, which is onShoot and it is a delegate. onShoot will be called in Shoot method. Actual logic related with Projectile is in ProjectileStandard class.

ProjectileStandard component is attached into Projectile_[WeaponName]. Let’s see how Projectile_Blaster prefabs look like.

<ProjectileStandard Component on Projectile_Blaster prefab>

<Projectile_Blaster>

Projectile has a radius for represent collision detection. You can see red sphere in the above image. That is a radius for collision detection.

Tuning for Torbjorn’s gun
If you know Overwatch (Blizzard’s game), there is a character Torbjorn. A little change makes feel different. I have just tuned some values to make a projectile to be same as Torbjorn primary gun.

<Change a scale to 1.7 from 10>

<Projectile looks much smaller than before>

Next, change Speed and Gravity Down Acceleration like down below.


<Change Speed and Gravity Down Acceleration>

If you change, the values like above then movements of projectile looks similar as Torbjorn’s primary gun projectile. In addition, WeaponControl’s Delay Between Shots value needs to be 0.5 from 0.1.

<original.gif>

<tune.gif>

Register callback
OnEnable method, we register OnShot method to the ProjectileBase’s onShot delegate. When somebody calls Shoot method in ProjectileBase then OnShot callback in ProjectileStandard is called.

Initial process for firing

<Firing Sequence>

When user fire the gun, several functions are called and onShot callback is called via delegate. In OnShot, we set shootTime, velocity, m_LastRootPosition and so on. Especially m_LastRootPosition is for tracking a position and it is used in collision detection.

Prevent firing a gun in front of the wall
User is able to fire a gun in front of the wall. When user is really close to the wall, bullet can go through walls. We need to prevent this.

if (Physics.Raycast(playerWeaponsManager.weaponCamera.transform.position, cameraToMuzzle.normalized, out RaycastHit hit, cameraToMuzzle.magnitude, hittableLayers, k_TriggerInteraction))
{
if (IsHitValid(hit))
    {
        OnHit(hit.point, hit.normal, hit.collider);
}
}


Collision Detection
Most important thing in projectile is collision detection in my opinion. Last time I mentioned that this class has a member ‘radius’. For collision detection, we use sphere shape. As you know bullet could be really fast which means it can go through the wall or enemy even player character. So just sphere vs plane or sphere vs sphere collision detection is not enough. We have to consider the time lapse.

<Time t can be really big if bullets are so fast>

In Unity has built-in functions for this and we can use it.

// Sphere cast
Vector3 displacementSinceLastFrame = tip.position - m_LastRootPosition;
RaycastHit[] hits = Physics.SphereCastAll(m_LastRootPosition, radius, displacementSinceLastFrame.normalized, displacementSinceLastFrame.magnitude, hittableLayers, k_TriggerInteraction);

foreach (var hit in hits)
{
if (IsHitValid(hit) && hit.distance < closestHit.distance)
{
foundHit = true;
closestHit = hit;
}
}

There exist multiple hits and we need to consider the closest hit.

Store m_LastRootPosition
At the end of Update method, we should store m_LastRootPosition like down below to track the last position of the projectile.

m_LastRootPosition = root.position;

Explosion and damage process
In OnHit method, we create particle, play SFX and destroy by itself. For damage process, it uses Damageable class. Using GetComponent, we get Damageable comp and then call InflictDamage function. If the projectile type is area (like bomb), calls InflictDamageInArea.

Class: EnemyController

When we talk about enemy (for AI programmer), usually enemy class has its AI states, behaviors. For instance, idle, move, attack, dead states. In this example, EnemyMobile and EnemyTurret has its own movement behavior and AI and controls Enemy with EnemyController.

EnemyController has no connection to EnemyMobile and EnemyTurret. Instead, EnemyMobile and EnemyTurret uses EnemyController. EnemyController has patrolPath, other utility functions that is common logic for both EnemyMobile and EnemyTurret.

Class: EnemyMobile

EnemyMobile is an AI class for Enemy HoverBot. EnemyMobile has 3 states which is Patrol, Follow, Attack.

Patrol data is come from the patrol game object.


PatrolPath class has pathNode. Enemy will follow the paths when the state is Patrol. As soon as enemy see the player then it change his state.

Adjusting sound pitch for movement
When the enemy moves, we change a pitch of audio based on the speed.

m_AudioSource.pitch =
Mathf.Lerp(PitchDistortionMovementSpeed.min, PitchDistortionMovementSpeed.max, moveSpeed / m_EnemyController.m_NavMeshAgent.speed);

Transitions
There are many different ways to implement FSM(Finite State Machine). By definition each state has its connections, and then connections has its condition.

If the state has no match connections then state execute his logic based on the state something like (entering, updating, exiting). In this bot example, transitions and update logics are separated to 2 functions which is UpdateAIStateTransitions and UpdateCurrentAIState. Personally, separating a transition and update logics are good idea.

void UpdateAIStateTransitions()
{
    // Handle transitions
    switch (aiState)
    {
        case AIState.Follow:
            // Transition to attack when there is a line of sight to the target
            if (m_EnemyController.isSeeingTarget && m_EnemyController.isTargetInAttackRange)
            {
                aiState = AIState.Attack;
                m_EnemyController.SetNavDestination(transform.position);
            }
            break;
        case AIState.Attack:
            // Transition to follow when no longer a target in attack range
            if (!m_EnemyController.isTargetInAttackRange)
            {
                aiState = AIState.Follow;
            }
            break;
    }
}

void UpdateCurrentAIState()
{
    // Handle logic
    switch (aiState)
    {
        case AIState.Patrol:
            m_EnemyController.UpdatePathDestination();
            m_EnemyController.SetNavDestination(m_EnemyController.GetDestinationOnPath());
            break;
        case AIState.Follow:
m_EnemyController.SetNavDestination(
m_EnemyController.knownDetectedTarget.transform.position);
m_EnemyController.OrientTowards(
m_EnemyController.knownDetectedTarget.transform.position);
            break;
        case AIState.Attack:                if(Vector3.Distance(m_EnemyController.knownDetectedTarget.transform.position, m_EnemyController.detectionSourcePoint.position) >= (attackStopDistanceRatio * m_EnemyController.attackRange))
{                   
m_EnemyController.SetNavDestination(
m_EnemyController.knownDetectedTarget.transform.position);
        }
            else
            {
                m_EnemyController.SetNavDestination(transform.position);
            }

m_EnemyController.OrientTowards(
m_EnemyController.knownDetectedTarget.transform.position);
              
m_EnemyController.TryAtack((m_EnemyController.knownDetectedTarget.transform.position - m_EnemyController.weapon.transform.position).normalized);
            break;
    }
}

Class: EnemyTurret

EnemyTurret has 2 AI states. Idle and Attack. It is too simple to analysis so I will skip it.

Class: Damageable

There is a Health class, which represent health of Player or Bot. To decrease player/bot health, there are two ways.

1.     Call TakeDamage method of health class.
2.     Through Damageable, call TakeDamage method of health class.

Number 2 used for projectiles. Number 1 used for player itself. (get damage by falling/environment or other reasons)


Add more feature

I will write it next article! 

Appendix

UnityAction
In this example uses UnityAction object. This is a delegate. If you haven’t heard about delegate, what about callback? Delegate has same idea like callback. Most different thing of delegate is it can call back many functions. Normally callback function is a single. Delegate we can register/add a callback functions like receiver/subscriber.


Final UML Diagram.
I noticed that this UML Diagram doesn't show everything of FPS microgame. When you are investigating/analyzing something, you don't need to look at everything. Concentrate on what you are looking/wanting. 



No comments:

Post a Comment

Task in UnrealEngine

 https://www.youtube.com/watch?v=1lBadANnJaw