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.
<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.
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