What do you reckon, did we need to have different speeds for patrolling and for chasing?
I definitely think that is a very good feature to have. Here was my solution (Before seeing what Sam did):
Add a second value to the Mover.MoveTo function. This value would be the speed of the movement. When the Fighter calls it he sets the move speed, or when the AIController calls it for patrolling, it sets it at a different speed.
edit: I was close!
That sort of feature is important for immersion and to make the look and feel of the game be more dynamic.
I took a different approach, rooted in how I approached the animation speed in the first place. I used models and animations from Mixamo, and the distance travelled in the animations I was using for my main character weren’t the distances travelled in the patrolling guard’s animations (patrolling zombie?)… but I’m lazy and want to use the same animator for all of my humanoids…
So… when you put the speed factors in the blend mode, I still kept them at .5 and 1… and then in the mover, I have a couple floats
[SerializeField] float AnimationWalkSpeed=3.5;
[SerializeField] float AnimationRunSpeed= 7.0;
Then in the update I set the anim.Speed variable to scale based on the animation speeds defined… if the speed is less than AnimationWalkSpeed then it scales down from .5… if it’s faster than walk speed then it scales down from run speed. Then in the slower zombie guards mover script, I just adjust the speeds.
This made setting two speeds absurdly easy, without having to change the MoveTo Method… I simply created two methods
public void SetPaceWalking() { navMesh.speed=AnimationWalkSpeed; }
public void SetPaceRunning() {navMesh.speed=AnimationRunSpeed;}
Then in each state, I call the appropriate SetPacexx() based on what’s going on.
Ok so I trow the stone here before watch any other solution, I did got bugget as well as Rick from that in the early stage of patroling so I already tuned it up. Based on my assumption I had a thinking on where would be better to control the speed of patroling and at first I’d put it on the enemy as a Serialized float, but after that I said to myself “what if I want to set up different patroling speed for different paths ?” For not make it too complicated then I just add code to PatrolPath script
[SerializeField] float patrolSpeed = 2f;
And inside the same class I put the GetPatrolSpeed method
public float GetPatrolSpeed()
{
return patrolSpeed;
}
My current Enemy NavSpeed is set to 4 so I would like a slower patroling first and then when it’s time to chase the Player I would like to set back the speed at default of Enemy. So inside the AiController script I add the reference to the original NavMeshAgent speed.
float originalNavMeshSpeed;
and I set it on the Start Method
originalNavMeshSpeed = GetComponent<NavMeshAgent>().speed;
Then we must only change the speed of Enemy while he is in Patroling and set back to normal while the is on the other state so inside Patroling I add the call to Mover method SetNavSpeed
public void SetNavSpeed(float speed)
{
navMeshAgent.speed = speed;
}
private void PatrolBehaviour()
{
Vector3 nextPosition = guardPosition;
if (patrolPath != null)
{
//If got a Patrol Path set the patrol speed
mover.SetNavSpeed(patrolPath.GetPatrolSpeed());
// rest of the code as normal lesson
so inside both the SuspicionBehaviour() and the AttackBehaviour() I just set the normal default speed
mover.SetNavSpeed(originalNavMeshSpeed);
And for me it’s working pretty well, only stuff that need to be watchfull is the height of the waypoints that sometimes make the Guard stuck
Yes, Definitely the Mover class is the place where I would put some code for the additional speed values. The problem is that we would like to have more generic approach that could use the following logic:
Normally we both for player and guards use standard, leisure speeds. Let us say 2.5 for guards would be plenty. I myself did some patrolling and was told that patrol speed is low. So, then would be chase speed , probably 3.5 or even 4. Then over run, like overdrive , that would be expensive but would give the guard possibly 5 or maybe even 5.6. He should be as fast as the player but only for maybe 3 seconds. Maybe two and then it could be upgraded later on or so…
The overdrive could also have effect on the hit power or time between attacks (mimicking the tiredness).
Also I would think of adding the slight delay between the normal patrol speed and the chase speed.
The delay as well as the actual values for the speed increase could be based on Random number function that could be slightly modified according to experience or character of the guard in question.
also the play er could have similar options.
My idea would be that the player would want to attack the guards when they are on their own but they would meet now and then. This happens in my game. I often get killed because they are meeting when I am busy with one of them. Also when attacked a guard would have an option to immediately chase the player or notify his friends The last one would also be at a price , like starting the chase later.
He would for example send the last known location of the player to the closest patrol man near him.
Later in the game he could even give approximation of speed and direction, so velocity. Or that would be an ability of an officer. So, CO could give velocity, NCO could give location and rank and file only give information of the encounter that they had spotted the enemy. Their own location would be known to the nearest friendly unit.
I went well ahead, this would rather belong in other section but I took liberty to share these ideas. Also possibly the player could find some allies somewhere. Possibilities are just dependent on imagination.
Absolutely. This is natural progression of complexity so getting closer to reality. I love this idea. This would beg for some introduction of energy levels.
I actually have some problems . First is that when my characters finish fighting they often but not always slide , they stay in last pose of the attack animation. I have used the Cancel and also the ActionScheduler but not working perfectly every time. The other problem is that I have the trouble noticing the effect of time between the attacks. In my game everyone, especially guards are hitting all the time.
I have player on 200 HP , 1 sec for time between attacks and he gets beaten if faced with two guards at the same time. I have used 10 points as weapon damage. Yet I trigger the attack, followed by mirrored attack, so effectively left and right hit. Then they keep on bludgeoning me so to speak .
They HP are less, 100 and I have gave them longer time delay but …
The next thing is that when my character moves I have some difficulty at pointing also at the enemies to order attack. I manage but it is not that easy. Many things but I have already fixed some issues. I have cameras working OK. I have introduced all in all four cycling cameras and ability to pan the area around the player. Also I pointed one of the cameras on the enemy and one on the critical location in the game. Two I have reserved for the player. Possibly one I will use for special events.
Edited Sat Jul 20 2019 09:49
This is my idea for giving characters a variety of speeds
(Note, for brevity I’ve only included the affected sections of my code for the Mover and AIControl scripts.)
Mover.cs
[SerializeField] float baseSpeed = 5f;
// TODO find out how to note inside unity baseSpeed is "feet per second"
public string characterSpeed = "idle";
SpeedModifier speedMod = new SpeedModifier();
struct SpeedModifier
{
const float Idle = 0f;
const float Amble = 0.5f;
const float Walk = 1f;
const float Hustle = 1.5f;
const float Jog = 2f;
const float Run = 3f;
public float GetSpeedModifier(string speed)
{
float speedMultiplier;
if (speed == "amble") { speedMultiplier = Amble; }
else if (speed == "walk") { speedMultiplier = Walk; }
else if (speed == "hustle") { speedMultiplier = Hustle; }
else if (speed == "jog") { speedMultiplier = Jog; }
else if (speed == "run") { speedMultiplier = Run; }
else { speedMultiplier = Idle ;}
return speedMultiplier;
}
}
public void MoveTo(Vector3 destination)
{
float speedMult = speedMod.GetSpeedModifier(characterSpeed);
agent.speed = baseSpeed * speedMult;
agent.destination = destination;
agent.isStopped = false;
}
I initially wanted to use an enum, but that wouldn’t work because enums only allow integers. So, I thought, “Maybe a struct will work.” (thanks @ben for that lesson in the FBullsandCows::BullsCowsGame section). While not as elegant as I had initially envisioned, the struct has kept the process of determining the modifier with the variables and allows me to reference them with a single line so, job done… I guess
PlayerControler.cs
(so much changed in this that I decided to just include the whole thing)
public class PlayerControler : MonoBehaviour
{
Mover mover;
Fighter fighter;
void Start()
{
mover = GetComponent<Mover>();
fighter = GetComponent<Fighter>();
}
void Update()
{
if (GetComponent<Health>().GetIsDead()) { return; }
if (InteractWithCombat()) { return; }
if (InteractWithMovement()) { return; }
print("Nothing to do joe");
}
private bool InteractWithCombat()
{
RaycastHit[] hits = Physics.RaycastAll(GetRay());
foreach (RaycastHit hit in hits)
{
CombatTarget candidate = hit.transform.GetComponent<CombatTarget>();
if (candidate == null) { continue; }
if (!fighter.CanAttack(candidate.gameObject)) { continue; }
if (Input.GetMouseButton(0))
{
fighter.Attack(candidate.gameObject);
}
return true;
}
return false;
}
private bool InteractWithMovement()
{
RaycastHit hit;
bool hasHit = Physics.Raycast(GetRay(), out hit);
if (hasHit)
{
if (IsValidInput())
{
int button = GetMouseButton();
if (button == 1) { mover.characterSpeed = "walk"; }
if (button == 2) { mover.characterSpeed = "run"; }
if (button == 3) { mover.characterSpeed = "amble"; }
if (button == 4) { mover.characterSpeed = "hustle"; }
if (button == 5) { mover.characterSpeed = "jog"; }
else { mover.characterSpeed = "idle"; }
mover.StartMoving(hit.point);
}
return true;
}
return false;
}
private int GetMouseButton()
{
if (Input.GetMouseButton(0)) { return 1; }
if (Input.GetMouseButton(1)) { return 2; }
if (Input.GetMouseButton(2)) { return 3; }
if (Input.GetMouseButton(3)) { return 4; }
if (Input.GetMouseButton(4)) { return 5; }
else { return 0; }
}
bool IsValidInput()
{
if (
Input.GetMouseButton(0) ||
Input.GetMouseButton(1) ||
Input.GetMouseButton(2) ||
Input.GetMouseButton(3) ||
Input.GetMouseButton(4)
)
{ return true; }
else
{ return false; }
}
private static Ray GetRay()
{
return Camera.main.ScreenPointToRay(Input.mousePosition);
}
}
would have been nice if I could have checked for multiple buttons with a single line. But alas, it was not to be and ended up far more complicated than I wanted
AIControler.cs
Mover mover;
private void Start()
{
// addition code removed
mover = GetComponent<Mover>();
}
private void AttackBehavior()
{
timeSinceLastSawPlayer = 0f;
mover.characterSpeed = "jog";
fighter.Attack(player);
}
private void ResumeRoutine()
{
Vector3 nextposition;
if (patrolPath == null) { nextposition = guardPosition; }
else { nextposition = GetNextWaypoint(); }
mover.characterSpeed = "amble";
if (patrolPath != null)
{
if (AtWaypoint())
{
dwellingAtWaypoint = 0f;
CycleWaypoint();
}
nextposition = GetNextWaypoint();
}
GetComponent<Mover>().StartMoving(nextposition);
}
What I would actually do is to create 2 float variables which would contain the speed for chasing and the speed for patrolling and when the enemy would start chasing me (in Fighter.cs where I’m sending my enemy to attack the player) I would use something like GetComponent().speed and set it to the chasing speed. Otherwise the speed would be set to the patrolling speed.
I used the ActionScheduler()
public void SetNavMeshSpeed(float Speed)
{
GetComponent<NavMeshAgent>().speed = Speed;
}
and used it from the AIController
[SerializeField] float patrolSpeed = 2.0f;
[SerializeField] float attackSpeed = 4.5f;
NavMeshAgent agent;
private void Awake()
{
.......
agent = GetComponent<NavMeshAgent>();
}
private void GuardBehaviour()
{
........
agent.speed = patrolSpeed;
........
}
private void AttackBehaviour()
{
........
agent.speed = attackSpeed;
........
}
I did it like this before watching Sam.
Yes, it is a very interesting feature, so the enemy´s behaviour gets more realistic.
I thought it should be a change in Fighter behaviour, changing the Nav Mesh Agent speed by an increase factor (serialized) when there is a target but not in attack range, returning to regular speed when the attack is cancelled:
namespace RPG.Combat
{
public class Fighter : MonoBehaviour, IAction
{
[SerializeField] float chaseSpeedIncreaseFactor = 1.5f;
UnityEngine.AI.NavMeshAgent navMeshAgent;
float regularSpeed;
.
.
.
private void Start()
{
.
.
.
navMeshAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
regularSpeed = navMeshAgent.speed;
}
private void Update()
{
timeSinceLastAttack += Time.deltaTime;
if (target == null) return;
if (!CanAttack(target.gameObject)) return;
if (!GetIsInRange( ))
{
if (navMeshAgent.speed == regularSpeed)
{
navMeshAgent.speed *= chaseSpeedIncreaseFactor;
}
.
.
.
public void Cancel()
{
target = null;
navMeshAgent.speed = regularSpeed;
StopAttack();
}
.
.
.
}
Okay!
Honestly I implement different speed for patroll and chasing during first steps - when we add patrol point, even before we add waypoints Dwelling, lol.
so… I add a variable:
[SerializeField] private float maxSpeed = 4.66f
also at CycleWayPoint() I add string:
m_NavMeshAgent.speed = 2f;
so it is a patrol speed, and them walk from point to point, but once Player is detected and AttackBehaviour() called - ther other string:
m_NavMeshAgent.speed = maxSpeed;
so - enemies run at max speed after the player.
All changes was made in AIController. And my controler has few differences from your because of all that challenges.
Im having a weird problem, for some reason, my maxSpeed in Unity 2021.3.33f1 it’s being set up to 0 when i set it up to the maxSpeed of 5.66f or 6f. Im sharing my code right below, but im just confuse by what’s happening, i don’t get why all of my prefabs and set up player and enemies have 0 of max speed.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using RPG.Core;
namespace RPG.Movement{
public class Mover : MonoBehaviour, IAction{
[SerializeField] Transform target;
[SerializeField] float maxSpeed = 6f;
NavMeshAgent navMashAgent;
Health health;
private void Start(){
navMashAgent = GetComponent<NavMeshAgent>();
health = GetComponent<Health>();
}
void Update(){
navMashAgent.enabled = !health.GetIsDead();
UpdateAnimator();
}
public void StartMoveAction(Vector3 destination, float speedFraction){
GetComponent<ActionScheduler>().StartAction(this);
MoveTo(destination, speedFraction);
}
public void MoveTo(Vector3 destination, float speedFraction){
navMashAgent.destination = destination;
navMashAgent.speed = maxSpeed * Mathf.Clamp01(speedFraction);
navMashAgent.isStopped = false;
}
public void Cancel(){
navMashAgent.isStopped = true;
}
private void UpdateAnimator(){
Vector3 velocity = navMashAgent.velocity;
Vector3 localVelocity = transform.InverseTransformDirection(velocity);
float speed = localVelocity.z;
GetComponent<Animator>().SetFloat("ForewardSpeed", speed);
}
}
}
I will also share my PlayerController, AIController and Fighter to see if someone can see the mistake
PlayerController:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RPG.Movement;
using RPG.Combat;
using RPG.Core;
namespace RPG.Control{
public class PlayerController : MonoBehaviour{
Health health;
Mover mover;
CombatTarget combatTarget;
Fighter fighter;
private void Start(){
health = GetComponent<Health>();
mover = GetComponent<Mover>();
combatTarget = GetComponent<CombatTarget>();
fighter = GetComponent<Fighter>();
}
void Update(){
if(health.GetIsDead()) return;
if(InteractWithCombat()) return;
if(InteractWithMovement()) return;
}
private bool InteractWithMovement(){
RaycastHit hit;
bool hasHit = Physics.Raycast(GetMouseRay(), out hit);
if(hasHit){
if(Input.GetMouseButton(0)) mover.StartMoveAction(hit.point, 1f);
return true;
}
return false;
}
private bool InteractWithCombat(){
RaycastHit[] hits = Physics.RaycastAll(GetMouseRay());
foreach(RaycastHit hit in hits){
CombatTarget target = hit.transform.GetComponent<CombatTarget>();
if(target == null) continue;
if(!fighter.CanAttack(target.gameObject)) continue;
if(Input.GetMouseButton(0)) fighter.Attack(target.gameObject);
return true;
}
return false;
}
private Ray GetMouseRay(){
return Camera.main.ScreenPointToRay(Input.mousePosition);
}
}
}
AIController:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RPG.Combat;
using RPG.Core;
using RPG.Movement;
namespace RPG.Control{
public class AIController : MonoBehaviour{
[SerializeField] float chaseDistance = 5f;
[SerializeField] float suspicionTime = 5f;
[SerializeField] PatrolPath patrolPath;
[SerializeField] float waypointTolerance = 1f;
[SerializeField] float waypointDwellTime = 5f;
[Range(0,1)]
[SerializeField] float patrolSpeedFraction = 0.2f;
GameObject player;
Fighter fighter;
Health health;
Mover mover;
Vector3 guardLocation;
float timeSinceLastSawPlayer = Mathf.Infinity;
float timeSinceArrivedAtWaypoint = Mathf.Infinity;
int currentWaypointIndex = 0;
private void Start(){
fighter = GetComponent<Fighter>();
player = GameObject.FindWithTag("Player");
health =GetComponent<Health>();
mover = GetComponent<Mover>();
guardLocation = transform.position;
}
private void Update(){
if(health.GetIsDead()) return;
if(InAttackRangeOfPlayer() && fighter.CanAttack(player)){
AttackBehaviour();
}else if(timeSinceLastSawPlayer < suspicionTime) {
SuspicionBehaviour();
} else PatrolBehaviour();
UpdateTimers();
}
private void UpdateTimers(){
timeSinceLastSawPlayer += Time.deltaTime;
timeSinceArrivedAtWaypoint += Time.deltaTime;
}
private void AttackBehaviour(){
timeSinceLastSawPlayer = 0;
fighter.Attack(player);
}
private void SuspicionBehaviour(){
GetComponent<ActionScheduler>().CancelCurrentAction();
}
private void PatrolBehaviour(){
Vector3 nextPosition = guardLocation;
if(patrolPath != null){
if(AtWaypoint()){
timeSinceArrivedAtWaypoint = 0;
CycleWaypoint();
}
nextPosition = GetCurrentWaypoint();
}
if(timeSinceArrivedAtWaypoint > waypointDwellTime) {
mover.StartMoveAction(nextPosition, patrolSpeedFraction);
}
}
private bool AtWaypoint(){
float distanceToWaypoint = Vector3.Distance(transform.position, GetCurrentWaypoint());
return distanceToWaypoint < waypointTolerance;
}
private void CycleWaypoint(){
currentWaypointIndex = patrolPath.GetNextWaypointIndex(currentWaypointIndex);
}
private Vector3 GetCurrentWaypoint(){
return patrolPath.GetWaypoint(currentWaypointIndex);
}
private bool InAttackRangeOfPlayer(){
float distanceToPlayer = Vector3.Distance(transform.position,
player.transform.position);
return distanceToPlayer < chaseDistance;
}
//Called by Unity
private void OnDrawGizmosSelected() {
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(transform.position, chaseDistance);
}
}
}
Fighter:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RPG.Movement;
using RPG.Core;
namespace RPG.Combat{
public class Fighter : MonoBehaviour, IAction{
[SerializeField] float weaponRange = 2f;
[SerializeField] float timeBetweenAttacks = 1f;
[SerializeField] float weaponDamage = 5f;
Health target;
float timeSinceLastAttack = Mathf.Infinity;
private void Update(){
timeSinceLastAttack += Time.deltaTime;
if(target == null) return;
if(target.GetIsDead()) return;
if (!GetIsInRange()){
GetComponent<Mover>().MoveTo(target.transform.position, 1f);
} else{
GetComponent<Mover>().Cancel();
AttackBehaviour();
}
}
private bool GetIsInRange(){
return Vector3.Distance(transform.position, target.transform.position) < weaponRange;
}
public bool CanAttack(GameObject target){
if(target == null) return false;
Health targetToTest = target.GetComponent<Health>();
return targetToTest != null && !targetToTest.GetIsDead();
}
public void Attack(GameObject combatTarget){
GetComponent<ActionScheduler>().StartAction(this);
target = combatTarget.GetComponent<Health>();
}
private void AttackBehaviour(){
transform.LookAt(target.transform);
if(timeSinceLastAttack > timeBetweenAttacks){
//This will trigger the Hit() event.
TriggerAttack();
timeSinceLastAttack = 0;
}
}
//Animations
private void TriggerAttack(){
GetComponent<Animator>().ResetTrigger("Stop Attack");
GetComponent<Animator>().SetTrigger("Attack");
}
private void TriggerStopAttack(){
GetComponent<Animator>().ResetTrigger("Attack");
GetComponent<Animator>().SetTrigger("Stop Attack");
}
//Animation Event
void Hit(){
if(target == null) return;
target.TakeDamage(weaponDamage);
}
public void Cancel(){
TriggerStopAttack();
target = null;
}
}
}