Ok I honestly should’ve asked for help about this earlier, but here we are. One of my main issues in my game that I recently noticed, is that my ranged weapons can hit through static in-game objects (for example, an arrow can go through a house like it doesn’t even exist). How do we fix this issue, in such a way that the player does not fire ranged weapons, if there are static objects in the way between the enemy I clicked on and the player, like a house or a tree…?
On your static objects, create a new layer, something like “Obstacle”, and assign that layer to the static objects.
Add a SerializeField LayerMask layerMask to the Fighter component. Test line of sight with this method before shooting.
bool HasLineOfSight()
{
Ray ray = new Ray(transform.position + Vector3.up, target.transform.position - transform.position);
if (Physics.Raycast(ray, out RaycastHit hit,
Vector3.Distance(target.transform.position - transform.position), layerMask))
{
return false;
}
return true;
}
is the Vector3 missing a second parameter…? (and I’m assuming the ‘layerMask’ is 'layerMask: “Obstacle” '… (compiler is complaining about these two)
I did miss the 2nd parameter on Distance, thats’ the transform.position.
The layerMask should just be layerMask, you set the layerMask in the inspector to the layer you want to test against (obstacle).
[SerializeField] private LayerMask layerMask;
bool HasLineOfSight()
{
Ray ray = new Ray(transform.position + Vector3.up, target.transform.position - transform.position);
if (Physics.Raycast(ray, out RaycastHit hit,
Vector3.Distance(transform.position, target.transform.position - transform.position), layerMask))
{
return false;
}
return true;
}
ok so I created the layerMask as a serialized variable, and assigned it to a newly created layerMask I called ‘Obstacle’ that I created and assigned to the houses in my scene (which I want to avoid stuff going through), but the issue persists. Any alternatives?
P.S: I also made sure the houses are assigned as ‘Static’
The houses also need colliders, for the Raycast to hit
Added a box collider, still same problem…
Where are you putting the call to HasLineOfSight?
good day Brian, it’s in the ‘Fighter.cs’ script
Edit: Oh, if we’re talking about where the function is called, I haven’t done that yet. Too many functions that might need it seem to exist, so my guess is it would be as an ‘if’ check statement under ‘Attack()’ in Fighter.cs, something like this:
if (!HasLineOfSight()) return;
(that didn’t work though, it returns errors. Not sure how to implement it tbh. I need some help)
Edit 2: Tried implementing the same line above in ‘Fighter.Shoot()’, still failed…
Edit 3: OK so apparently placing this line of code in ‘Fighter.AttackBehaviour()’ makes sure the player won’t attack an enemy if there’s an obstacle in the way, but it also has a problem that I’ll mention below:
if (!HasLineOfSight() && currentWeaponConfig.HasProjectile()) {
return;
}
The problem is… the attack won’t happen (but is registered) unless the player and the enemy are in line of sight. It can get cancelled, but it’ll keep the enemy registered until he’s in line of sight… How do we get the player to run around until there’s a line of sight visible (only if there’s a path… so how do we also make sure he doesn’t spin around the world as a result of trying to find a way to attack that enemy?
Edit 4: OK I solved it. This is my final function, which I placed at the top of my ‘Fighter.AttackBehaviour()’ function (not gonna lie, low-key I feel proud of myself for solving something, even if it’s a small thing, but also please tell me if there’s any better way that we can use to solve this, in case something happens down the line), but it also has a small concern from my side:
if (!HasLineOfSight() && currentWeaponConfig.HasProjectile()) {
while (!HasLineOfSight()) {
GetComponent<Mover>().MoveTo(target.transform.position, 1);
return;
}
}
My concern is… What if there is no way to reach the enemy because the game wasn’t designed this way, how do we make sure that my player doesn’t roam the entire game world trying to figure this problem out, and just pushes out an error saying it’s out of reach?
Edit 5: To counter for the previous problem, I added a check for ‘CanMoveTo()’ in my while loop. It works, but because I have no way of confirming this at the moment, I want to reconfirm that it’s the correct one:
if (!HasLineOfSight() && currentWeaponConfig.HasProjectile()) {
while (!HasLineOfSight() && GetComponent<Mover>().CanMoveTo(target.transform.position)) {
GetComponent<Mover>().MoveTo(target.transform.position, 1);
return;
}
}
Not bad, not bad at all!
ha ha! - I was happy for a solid hour after that … on to the next problem(s)
OK I hope this isn’t too late, but I just noticed that while the player definitely needs to have a sight of vision to be able to attack the enemy, the enemy does not have the same condition, and can attack the player through static objects, like they don’t exist. How do we go around fixing this? (I’m guessing we will take the check condition and place it in another function, I’m just not sure which one)
Since the Fighter is the same for both the Player and the Enemy, I have absolutely no idea why this isn’t working for the enemies.
Paste in your complete Fighter.cs script.
Hi Brian. Sure thing, here is my Fighter.cs script (it’s fairly long though, be warned ):
using UnityEngine;
using RPG.Movement;
using RPG.Attributes;
using GameDevTV.Saving; // namespace for us to access 'ISaveable' (so we can save our weapons as we transition between scenes)
using RPG.Control; // namespace to access 'AIController.cs', for when we want to avoid guards from attacking our players if we are in a cinematic sequence
using RPG.Stats; // so we can damage our enemy based on the weapon we have and our combat level
using System.Collections.Generic; // so we can use 'IEnumerable' to add extra special weapon damage, fulfilling 'IModifierProvider'
using GameDevTV.Utils;
using RPG.Core;
using GameDevTV.Inventories; // Ensures we can deal with the weapons we pickup off the ground as actual weapons
namespace RPG.Combat {
public class Fighter : MonoBehaviour, IAction // ISaveable, IModifierProvider
// IAction is a contract that ensures this class has a 'Cancel()' function (same as what's in the IAction interface)
// ISaveable is a contract that ensures 'CaptureState()' and 'RestoreState(object state)' functions exist in this script
{
[SerializeField] Transform rightHandTransform = null; // the player right hand, where we can put the sword (or any other right-handed weapon) for him to wield
[SerializeField] Transform leftHandTransform = null; // the player left hand, where can put our ranger bow (or any other left-handed weapon) for him to wield
[SerializeField] WeaponConfig defaultWeapon = null; // the default Weapon for our fighter (his bare hands)
[SerializeField] bool autoAttack = false; // (OUT OF COURSE CONTENT) this boolean automatically fights enemies back that attack our player
[SerializeField] float autoAttackRange = 4.0f; // The Range our Player can automatically fight back (any hits beyond that are not autoAttackable to the player)
[SerializeField] LayerMask layerMask; // The layer Mask is used in 'AttackBehaviour()', to ensure our Player can't use ranged weapons through obstacles, such as houses lying between him and his enemies
Health target; // Enemy (to attack), of type health (so we are specific that we are targeting our enemy's health)
Equipment equipment; // to subscribe to when our player puts equipment on him (in void Awake())
float timeSinceLastAttack = Mathf.Infinity; //Updated (through void Update()), to help for times when our player is not in combat (for animations)
WeaponConfig currentWeaponConfig; // the weapon our player is currently holding in hand
LazyValue<Weapon> currentWeapon; // the weapon we are using to hit (so we can add audio to it)
// ----------------------------- OUT OF COURSE CONTENT: Stuff for Dual-Handed and 2h weapons ----------------------------
/* ShieldConfig currentShield;
ShieldConfig defaultShield;
LazyValue<Shield> currentEquippedShield; */
// ----------------------------------------------------------------------------------------------------------------------
private void Awake() {
GetComponent<Health>().onTakenHit += OnTakenHit; // subscribes the hit event to our player, so
// that if he gets attacked while shopping, he closes
// the shop and focuses on the fight
currentWeaponConfig = defaultWeapon;
currentWeapon = new LazyValue<Weapon>(SetupDefaultWeapon);
// ------------------------------------ MORE OUT OF COURSE CONTENT --------------------------
/* currentShield = defaultShield;
currentEquippedShield = new LazyValue<Shield>(null); */
// ------------------------------------------------------------------------------------------
equipment = GetComponent<Equipment>();
if (equipment) {
// Update your weapon in your players' head, based on what he has on him
equipment.equipmentUpdated += UpdateWeapon;
}
}
private bool AutoAttack() {
// This function gets the AutoAttack() of the player, to be used in 'OnTakenHit()' below
return autoAttack;
}
private void OnTakenHit(GameObject enemy) {
// This function checks if AutoAttack() is enabled. If true,
// the player will automatically attack aggressive enemies back.
// If false, the player will simply take the hits as they come along
if (AutoAttack() == true) {
if (target != null) return;
if (CanAttack(enemy)) Attack(enemy);
}
}
private Weapon SetupDefaultWeapon() {
// This function makes the default weapon of our player, if he has nothing wielded otherwise, in his hand (his fists in this case)
return AttachWeapon(defaultWeapon);
}
private void Start() {
currentWeapon.ForceInit();
}
private void Update() {
timeSinceLastAttack += Time.deltaTime; // Updates the time since last attack with the time taken to last render a game frame
if (target == null) return; // Stops the function from working if we don't have a target at the start itself (no fight occuring)
// (this ensures that if we are fighting, we will neither stop nor move, unless clicked upon externally)
// The 'if' statement above solves the issue that we will always Stop moving (caused by GetComponent<Mover>().Stop() below)
// if we don't have a target, as shown below:
if(target.IsDead()) {
// After killing our Target, we want to find new targets in range before deactivating automatic attack
target = FindNewTargetInRange();
if (target == null) return; // If our target is dead (bool function in Health.cs), we stop beating him up
}
// The following lines apply if we are in a fight:
if (!GetIsInRange(target.transform)) {
GetComponent<Mover>().MoveTo(target.transform.position, 1.0f); // if there is a target, and it's not in Range, we go to it, at speedFraction = 1.0f (maxSpeed)
}
else {
GetComponent<Mover>().Cancel(); // Stops moving to our target if it's either non-existent, or it's closer than weaponRange
AttackBehaviour();
}
}
private Health FindNewTargetInRange()
{
Health best = null; // if no enemy is caught up, we don't autoAttack anyone
float bestDistance = Mathf.Infinity;
foreach (var candidate in FindAllTargetsInRange()) {
// Find the shortest distance between a 'candidate' (enemy) and the player (or whoever holds this script)
float candidateDistance = Vector3.Distance(transform.position, candidate.transform.position);
// if the caught up enemy is the closest, we go attack that, and update our best Candidate to be that enemy
if (candidateDistance < bestDistance) {
best = candidate;
bestDistance = candidateDistance;
}
}
return best;
}
private IEnumerable<Health> FindAllTargetsInRange() {
// Sphere cast around Player location:
RaycastHit[] raycastHits = Physics.SphereCastAll(transform.position, autoAttackRange, Vector3.up);
// Iterate over the Raycast Hits:
foreach (var hit in raycastHits)
{
// for every health component you catch, avoid the 3 conditions below the next line:
Health health = hit.transform.GetComponent<Health>();
// if the target has no health, is dead or it is the script holder (player/enemy in this case) avoid the autoAttack
if (health == null) continue;
if (health.IsDead()) continue;
if (health.gameObject == gameObject) continue;
yield return health;
}
}
public Health GetTarget () {
// Returns our Enemy (Players' Target) Health, so we can
// display it in-game
return target;
}
public Transform GetHandTransform(bool isRightHand) {
// This method allows us to automatically, based on
// what the designer chose, pick whether the weapon we have
// must be in our right hand, or our left hand
// (used in SpawnProjectileEffect.cs, for our 'abilities' section)
if (isRightHand) return rightHandTransform;
else return leftHandTransform;
}
private void AttackBehaviour() {
// This function, as the name suggests, consists of all the operations that occur when the player
// attacks an enemy (but not the other way around)
// ----------------------------------------- OUT OF COURSE STATEMENT: The following code checks for obstacles in the way of Ranged attacks. If they exist, we run to areas where they don't exist before firing Ranged weapons -----------------------------------------
if (!HasLineOfSight() && currentWeaponConfig.HasProjectile()) {
while (!HasLineOfSight() && GetComponent<Mover>().CanMoveTo(target.transform.position)) {
GetComponent<Mover>().MoveTo(target.transform.position, 1);
return;
}
}
// ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
transform.LookAt(target.transform); // Makes our player look at the enemy he's aiming to attack
// The following if statement has been edited outside of courseWork, where I want the timeBetweenAttacks to rely on the weapon rather than player
// To revert, cut 'timeBetweenAttacks' from 'Weapon.cs', paste above with other SerializedFields, and
// change 'currentWeapon.timeBetweenAttacks' back to 'timeBetweenAttacks' in the 'if' statement below
if (timeSinceLastAttack > currentWeaponConfig.timeBetweenAttacks) {
// Triggering the Attack Event (and resetting the timer to next attack):
TriggerAttack();
timeSinceLastAttack = 0; // Resets timer to calculate how long we need to wait before replaying the attack animation
}
}
private void TriggerAttack() {
// This function ensures the attack bug that is caused everytime we return to a fight, after interrupting the previous one, is eliminated,
// by deactivating/resetting the 'stopAttack' animation trigger,
// and it also initiates the Attacking animation (by activating the 'attack' trigger)
GetComponent<Animator>().ResetTrigger("stopAttack"); // Deactivates the stopAttack (which normally activates each time
// we return to a fight after interrupting the previous one
// (This stops an attack glitch from working!))
GetComponent<Animator>().SetTrigger("attack"); // Initiates the 'Attack' animation trigger (when enough time between attacks has passed by)
}
void Hit() {
// This function is only used in the AttackBehaviour() function, and
//it ensures damage is slightly delayed until the player punches the enemy
// It also checks if our current weapon has a Projectile or not (in case our player is a ranger)
// If he does, we launch our projectile (E.g: arrows)
// If not, just do regular melee damage (as usual)
if(target == null) return; // ensures no errors occur if the attack animation is playing, but we can't find a target
// (like when we walk away from an incomplete attack animation: this produces an error, and this is the solution)
float damage = GetComponent<BaseStats>().GetStat(Stat.Damage);
// Defending our Player from damage done by target, based on the shield or item he's wearing:
BaseStats targetBaseStats = target.GetComponent<BaseStats>();
if (targetBaseStats != null) {
float defence = targetBaseStats.GetStat(Stat.Defence);
// OLD Damage Formula (Random Hits):
/* damage *= Random.Range(0f, 1.25f); // randomizing the hits
if (Random.Range(0,100) > 95) { damage *= 2.0f; } // critical hit (i.e: double-hit, but under rare circumstances!) */
// NEW Damage Formula (Constant):
//damage /= (1 + defence/damage);
// NEW Damage Formula (Randomized):
// This formula reduces the effectiveness of weapons as the player gets stronger:
damage /= (1 + (defence/damage)); // Damage Formula (Power = Power * (1 + defence/enemy damage))
damage *= Random.Range(0, 1.25f); // Randomizing the Damage
if (Random.Range(0,100) > 95) damage *= 2.0f; // Critical hit
}
if (currentWeapon.value != null) {
currentWeapon.value.OnHit(); // returns the type of weapon we just picked up (so we can play audio accordingly)
}
if (currentWeaponConfig.HasProjectile()) {
currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, target, gameObject, damage); // if you have a projectile, launch it (from the instigators' hand, dealing damage based on what it has)
}
else {
target.TakeDamage(gameObject, damage); // Damages the enemy/player, based on our Damage Stats (in 'BaseStats.cs')
}
}
void Shoot() {
// In very simple terms, this function is the 'Hit()' function for
// Ranged (Projectile) Weapons, to be called in 'Projectile.cs'
Hit();
}
private bool GetIsInRange(Transform targetTransform) {
// This function checks if our player is within weapon range to our target enemy or not.
// If it's within weapon range, we stop moving to it (and we fight it), otherwise we pursue it.
// (Instead of forcing it into our code, it's something optional to be used when we actually have a fighting target.
// The reason this was made into a function is because in the if statement in the Update function now, it won't even
// bother to be called if the 'target != null' is false (as && stops working the moment the first one is false), hence
// avoiding compiler errors)
return Vector3.Distance(transform.position, targetTransform.position) < currentWeaponConfig.GetRange();
// Calculates the distance between Player (transform) and Target (target), and if it's less than our CurrentWeaponRange (i.e: STOP GETTING
// CLOSER), it returns true (otherwise false), and hence we can attack our enemy/player (when he's close enough to be hit)
}
public void Cancel() {
// Cancelling an attack (or other player operations)
StopAttack(); // Resets the attack trigger (to avoid any glitches if we unexpectedly quit a fight), and then it Stops attacking the enemy
target = null; // Sets our target to be empty, so we can runaway if we want to cancel our attack
GetComponent<Mover>().Cancel(); // for our CinematicControlRemover to fully remove any player controls when the CutScene starts, we also need to cancel
// any current movements to any targets we have assigned to our player, hence this line of code
}
private void StopAttack() {
// This function ensures no glitches occur when we stop the Attack animation from playing (when we are exiting a fight)
GetComponent<Animator>().ResetTrigger("attack"); // Deactivates the attack animation, when we return to a fight, in case it was
// incomplete earlier (similar to what we did to "stopAttack" in "TriggerAttack"
// earlier)
GetComponent<Animator>().SetTrigger("stopAttack"); // Stop attacking the enemy (first step)
}
/* public IEnumerable<float>GetAdditiveModifiers(Stat stat) {
if (stat == Stat.Damage) {
yield return currentWeaponConfig.GetDamage();
// Additive modifier damage (probably gotten by a potion or something), on top of the damage our character does
}
}
public IEnumerable<float>GetPercentageModifiers(Stat stat) {
if (stat == Stat.Damage) {
yield return currentWeaponConfig.GetPercentageBonus();
}
} */
public void Attack(GameObject combatTarget) {
// Without the IAction Contract inheritance, this line just won't work (Refer to Lecture 31)
GetComponent<ActionSchedular>().StartAction(this); // Uses the ActionSchedular Middleman (between this script and IAction Contract) to initiate an attack
target = combatTarget.GetComponent<Health>(); // our target to attack right now is our enemy's health
}
public bool CanAttack(GameObject combatTarget)
{
// This function checks if we have a combat target or not. If we don't, we quit the function.
// If we have a combat target, we let the game know he exists, and he's not dead (hence we can attack him)
if(combatTarget == null) { return false; }
// The only reason to stop attack now, is if the target is out of reach, and out of range (for in-game rangers and mages)
if (!GetComponent<Mover>().CanMoveTo(combatTarget.transform.position) && !GetIsInRange(combatTarget.transform)) { return false; }
Health targetToTest = combatTarget.GetComponent<Health>();
return targetToTest != null && !targetToTest.IsDead();
}
public void EquipWeapon(WeaponConfig weapon) {
// This function checks if our player has a weapon in hand or not. If not, ignore this function
// If our player has a weapon in hand, we want to Spawn it into his hand, and play the correct animation
// to our weapon
// if (weapon == null) return; // we have defaultWeapon to cover up for our null, so we don't need to check against null anymore
currentWeaponConfig = weapon; // the player's currentWeapon is whatever he's currently holding in his hand
currentWeapon.value = AttachWeapon(weapon);
}
// ------------------------ MORE OUT OF COURSE CONTENT ------------------------------------
bool HasLineOfSight() {
Ray ray = new Ray(transform.position + Vector3.up, target.transform.position - transform.position);
if (Physics.Raycast(ray, out RaycastHit hit, Vector3.Distance(transform.position, target.transform.position - transform.position), layerMask)) {
return false;
}
return true;
}
// ----------------------------------------------------------------------------------------
private void UpdateWeapon() {
// 1. Get the weapon from Equipment, and cast it to WeaponConfig
var weapon = equipment.GetItemInSlot(EquipLocation.Weapon) as WeaponConfig;
// 2. Equip Weapon
if (weapon == null) {
EquipWeapon(defaultWeapon); // if the weapon was null (i.e: Player was unarmed), equip the default weapon (the 'unarmed' weapon we created in Resources of weapons)
}
else {
EquipWeapon(weapon); // if the player had a weapon on him, replace it with the new weapon in our Weapon equipment slot
}
}
// ----------------------------------------- TUNED UpdateWeapon(), OUT OF COURSE CONTENT ------------------------------
/* private void UpdateWeapon() {
var weapon = equipment.GetItemInSlot(EquipLocation.Weapon) as WeaponConfig;
var shield = equipment.GetItemInSlot(EquipLocation.Shield) as ShieldConfig;
if (weapon == null) EquipWeapon(defaultWeapon);
else EquipWeapon(weapon);
if (shield != null) EquipShield(shield);
else DestroyOffHandItem();
} */
// ---------------------------------------------------------------------------------------------------------------------
public Weapon AttachWeapon(WeaponConfig weapon) {
Animator animator = GetComponent<Animator>();
return weapon.Spawn(rightHandTransform, leftHandTransform, animator);
}
}
}
Nope, doesn’t make sense at all that the Enemy isn’t stopped by the LineOfSight code.
This is one I don’t have an answer for.
Well… I do have one idea, as I work through the conditions…
If the archer doesn’t have line of sight, and cannot move to the location, then it will pass the check and shoot…
Let’s refactor this a bit…
//Reversing order of the check short circuits the if, HasLineOfSight is only tested
//when the currentWeaponConfig.HasProjectile is true, saves a LOT of clock cycles.
if(currentWeaponConfig.HasProjectile() && !HasLineOfSight())
{
//Since you're returning within the while loop, it's not appropriate, and more to the point
//While should NEVER be used outside of a Coroutine or async method, and then must
//include a yield return or await statement or you will freeze your game.
//The reason it didn't lock you up here is because you return in the middle, making it
// a glorified if.
// No need to retest HasLineOfSight, because you can only get to this statement when
// HasLineOfSight is false
if(GetComponent<Mover>().CanMoveTo(target.transform.position))
{
GetComponent<Mover>().MoveTo(target.transform.position, 1f);
}
return;
}
//Rest of method.
Well that refactor didn’t work too well… It didn’t solve the enemy hitting through a home issue, and it had my player come swooping in like a ninja around houses to hit the enemies (it’s kinda cool, NGL, but not what I’m looking for right now)
And the return; at the end of the refactoring did try to eliminate the rest of the function (so I deleted it)
While certainly amusing, I still don’t understand why it exhibits different behaviour for players than enemies… Or why the player would move faster than the normal speed…
he didn’t move faster than normal speed, he just came sliding in like a ‘WAJAA’ sorta ninja in style.