OK I know I’m everywhere all of a sudden today, but I decided to delay the wanted system for a day until I figure out how to reactivate my homing arrows (I should’ve done this sooner!), because right now firing arrows using this function:
// Called on the ranged animation event lines:
private void Shoot()
{
// (TEMP) Temporarily here, to ensure ghost enemies deal no damage and mess with the NPC hunting for enemies system:
if (GetComponent<Health>().IsDead()) return;
// In very simple terms, this function is the 'Hit()' function for
// Ranged (Projectile) Weapons, to be called in 'Projectile.cs' (for simplicity in naming's sake)
// Hit();
// RPG to Third Person Conversion changes are what the rest of this function is all about:
if (!currentWeaponConfig.HasProjectile()) return;
if (TryGetComponent(out ITargetProvider targetProvider))
{
float damage = GetDamage();
GameObject targetObject = targetProvider.GetTarget();
if (targetObject == null && GetComponent<EnemyStateMachine>())
{
targetObject = GameObject.FindWithTag("Player");
}
// TEST:
// damage *= UnityEngine.Random.Range(0f, 1.25f);
// if (UnityEngine.Random.Range(0f, 100f) > 95f) damage *= 2.0f;
// TEST (DELETE THE IF STATEMENT AND ITS CONTENTS, AND THE 'currentAnimal' AND ITS LINKS, IF FAILED):
if (GetComponent<PlayerStateMachine>()) {
// Use the 'currentAnimal' for the PLAYER ONLY
if (targetObject != null)
{
// use the 'LaunchProjectile' function with 5 arguments (as we do have a target)
currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, targetObject.GetComponent<Health>(), gameObject, damage);
}
else
{
// use the 'LaunchProjectile' function with 4 arguments (as we don't have a target), the last one (added 14/6/2024) is to offset the arrows based on the animal being driven
currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, gameObject, damage);
}
// Either way, you just fired an arrow, so use an ammo ('CheckAmmoQuantity' won't let you get this far without having arrows, so keep that in mind)
if (currentWeaponConfig.GetAmmunitionItem())
{
GetComponent<Quiver>().SpendAmmo(1);
}
}
else
{
// Normal action for the enemy and other NPCs (who don't have a 'CurrentAnimal' - TEMPORARY SOLUTION)
if (targetObject != null)
{
// use the 'LaunchProjectile' function with 5 arguments (as we do have a target)
currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, targetObject.GetComponent<Health>(), gameObject, damage);
}
else
{
// use the 'LaunchProjectile' function with 4 arguments (as we don't have a target), the last one (added 14/6/2024) is to offset the arrows based on the animal being driven
currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, gameObject, damage);
}
// If you have enough ammo, fire and use the ammo
if (currentWeaponConfig.GetAmmunitionItem())
{
GetComponent<Quiver>().SpendAmmo(1);
}
}
}
}
(It’s modified, I know)
Fires straight arrows that are not homing, regardless of whether the projectile itself is set to homing or not (CORRECTION, IT DOESN’T DO ANY SORT OF HOMING WORK IN FREELOOK MODE… I’LL DEVELOP SOMETHING FOR THAT)
It does work fine for targeting state though, but I also want to do the same for my new ranger aiming state, where I can get homing arrows to… aim for whoever is under the aim reticle
Can someone please review this for me?
(If it helps, here’s my entire ‘Projectile.cs’ script as well):
// If you plan to make a destructive environment of some sort, and maybe shoot it with projectiles,
// make sure to give it a specific layer and go to 'Edit -> Project Settings -> Physics', and in
// the big triangular matrix at the bottom, tick 'Projectile/(Whatever your layer name is)'
using UnityEngine;
using RPG.Attributes; // to access 'Health.cs'
using UnityEngine.Events;
using RPG.Skills;
using RPG.Movement;
using System.Collections.Generic;
using System.Collections;
using RPG.Animals;
using RPG.States.PlayerOnAnimal;
namespace RPG.Combat {
public class Projectile : MonoBehaviour
{
[SerializeField] bool isHoming = true; // determines if our missiles are homing (persuasive) or not
[SerializeField] float speed = 1.0f; // The speed of our Arrow being fired at its target
[SerializeField] GameObject hitEffect = null; // the hit effect when a projectile hits its target (particle system)
[SerializeField] float maxLifeTime = 10.0f; // the maximum lifetime of our Projectile
[SerializeField] GameObject[] destroyOnHit = null; // an array of objects to be destroyed once a Projectile hits an object
[SerializeField] float lifeAfterImpact = 2.0f; // the life of a Projectile after it has impacted a target
[SerializeField] UnityEvent onHit; // projectile hitting target audio
[Range(0f, 0.5f)]
[Tooltip("How hard is the knockback from the impact with one of these projectiles?")]
[SerializeField] float projectileKnockbackFactor; // how badly does this projectile knock someone back, based on dealt damage?
[Tooltip("If the speed of the arrow, in 'ShieldParryingSystem.cs', for any reason is set to zero when the shield parries the arrow, use this value instead")]
[SerializeField] float alternativeSpeedVariable;
Health target = null; // The target of our Projectile must have health for us to attack them
GameObject instigator = null; // Instigator (for us to gain xp as we kill enemies)
float damage = 0.0f; // Damage done to the target of our projectile (passed in from 'WeaponConfig.cs')
Vector3 targetPoint; // the target position of our Ranged Abilities
private void Start() {
// When firing an arrow, the first thing we want to do
// is look at our target (once only. If we put this
// in update, it will turn into a Homing Missile)
// transform.LookAt(GetAimLocation());
// For third person, we need this:
if (target) transform.LookAt(GetAimLocation());
}
// Update is called once per frame
void Update()
{
// This function shoots the arrow at a target. First, we check
// if we have a target or not. If there's no target,
// quit this function.
// If we have a target, and he's not dead, we want the arrow
// to look at our target, and then move towards it (on the Z-axis),
// via Vector3.forward, at the given Speed variable,
// over Time.deltaTime (time in frames per second)
// if (target == null) return;
if (target != null && isHoming && !target.IsDead()) {
transform.LookAt(GetAimLocation()); // makes our missile a homing missile
// (only if we assigned it as a homing missile)
// after we checked that the target is not dead,
// and our missile is homing
}
transform.Translate(Vector3.forward * speed * Time.deltaTime);
}
Skill skill;
public void SetTarget(Health target, GameObject instigator, float damage, Skill skill) {
// This function calls 'SetTarget' below, so we can aim at our enemies using our abilities
SetTarget(instigator, damage, target, Vector3.zero, skill);
}
public void SetTarget(Vector3 targetPoint, GameObject instigator, float damage, Skill skill) {
SetTarget(instigator, damage, null, targetPoint, skill);
}
/// <summary>
/// This function sets the target of our projectile. 1. It sets the target the projectile is aiming at, 2. it keeps track of him,
/// 3. It sets the damage destined to the projectile target, 4. It keeps track of who is responsible for firing that projectile, and 5.
/// it sets the skill that will get the XP for firing that hit
/// </summary>
/// <param name="instigator"></param>
/// <param name="damage"></param>
/// <param name="target"></param>
/// <param name="targetPoint"></param>
/// <param name="skill"></param>
public void SetTarget(GameObject instigator, float damage, Health target = null, Vector3 targetPoint = default, Skill skill = Skill.Ranged) {
this.target = target;
// our Target is now the GameObject holding this script
this.targetPoint = targetPoint;
// keep track of the target we are firing our Projectile abilities at
this.damage = damage;
// The damage dealt by our Projectile = damage from SetTarget ('float damage' argument)
this.instigator = instigator;
// The instigator is set to be an instance of whoever holds this script
this.skill = skill;
// The skill to train when a projectile is fired
Destroy(gameObject, maxLifeTime); // destroys our Projectile after its maximum lifetime (so it doesn't take a toll on our computer)
}
private Vector3 GetAimLocation() {
if (target == null) return targetPoint; // if there is no target, just head to the targetPoint
// This function aims the projectile at the heart (Z-Axis) of the target, by getting the target's
// Capsule Collider height, and then adding 1/2 of its height, so our arrow lands into the target's chest
// (midway through the CapsuleCollider). It also checks if the Target has a CapsuleCollider or not. If not, just aim the
// arrow at the target feet (its original aim point)
// CapsuleCollider targetCapsule = target.GetComponent<CapsuleCollider>();
CharacterController targetController = target.GetComponent<CharacterController>();
if (targetController == null) {
return target.transform.position;
}
return target.transform.position + Vector3.up * targetController.height / 2;
}
// Since I have two colliders on my player (one Character Controller, and a Capsule Collider for my Animal-Mounting System)
// the damage gets dealt twice. To combat that, this list ensures that the health component is only accessed by the Projectile once
// so the damage happens once only (done in the 'OnTriggerEnter' function below):
List<Health> alreadyHit = new List<Health>();
// New Code, for Third Person projectiles:
private void OnTriggerEnter(Collider other)
{
IAnimal animal = other.GetComponentInParent<IAnimal>(); // you can also use the 'PlayerOnAnimalStateMachine.cs' type here if you want
if (animal != null)
{
Animal currentAnimal = animal.GetAnimal(); // the animal currently driven by the player
PlayerOnAnimalStateMachine playerStateMachine = instigator.GetComponent<PlayerOnAnimalStateMachine>(); // the player
if (playerStateMachine != null && currentAnimal == playerStateMachine.GetAnimal())
{
// this animal this arrow hit = animal driven by player, and instigator = player?
// Ignore the arrow from the driver (i.e: the player)
Debug.Log($"Player is attempting to hit the animal he's mounting, ignore");
return;
}
}
else Debug.Log($"Projectile hit {other.gameObject.name}"); // Safety Log
// The following if statement is for cases when the enemy needs to dodge a projectile:
if (other.TryGetComponent<IncomingRangedAttackDetector>(out var detector))
{
if (detector.parentStateMachine.gameObject == instigator)
{
Debug.Log($"This is my projectile, ignore");
}
else
{
Debug.Log($"An outsider is trying to attack me, I should defend myself");
if (detector.GetComponentInParent<Health>().IsDead())
{
Debug.Log($"I am dead, can't do anything now");
return;
}
Vector3 randomDirection = Vector3.zero;
int randomRoll = Random.Range(0, 4);
switch (randomRoll)
{
case 0:
randomDirection = new Vector3(-1, 0, 0);
break;
case 1:
randomDirection = new Vector3(1, 0, 0);
break;
case 2:
randomDirection = new Vector3(0, -1, 0);
break;
case 3:
randomDirection = new Vector3(0, 1, 0);
break;
}
detector.parentStateMachine.SwitchState(new EnemyDodgingState(detector.parentStateMachine, randomDirection));
}
return;
}
// This function tells the projectile holding this script what to do
// when it hits something, depending on whether it's a target, or just
// something in the way
// if he's dead, just keep going. Don't try to damage him again...:
// if (other.GetComponent<Health>().IsDead()) return;
if (other.gameObject == instigator) return; // don't hit yourself
// if (other.TryGetComponent(out Health health)) health.TakeDamage(instigator, damage, instigator.GetComponent<Fighter>().GetCurrentWeaponConfig().GetSkill());
// TEST (if failed, uncomment the line above)
if (other.TryGetComponent(out Health health) && !alreadyHit.Contains(health))
{
if (other.GetComponent<Health>().IsDead()) return;
alreadyHit.Add(health);
// Randomize the Damage:
// damage *= UnityEngine.Random.Range(0f, 1.25f);
// if (UnityEngine.Random.Range(0,100) > 95) damage *= 2.0f;
health.TakeDamage(instigator, damage, instigator.GetComponent<Fighter>().GetCurrentWeaponConfig().GetSkill());
}
if (other.TryGetComponent(out ForceReceiver forceReceiver))
{
forceReceiver.AddForce(transform.forward * damage * projectileKnockbackFactor, true); // multiplied by 0.5f so as to neutralize the knockback impact
}
speed = 0;
onHit.Invoke();
if (hitEffect != null) Instantiate(hitEffect, transform.position, transform.rotation);
foreach (GameObject toDestroy in destroyOnHit) Destroy(toDestroy);
Destroy(gameObject, lifeAfterImpact);
Debug.Log($"Instigator = {instigator.name}");
Debug.Log($"Collider = {other.name}");
Debug.Log($"Damage Dealt = {damage}");
}
// Class needed for third person transition (nullifying the targets):
public void SetupNoTarget(GameObject instigator, float damage)
{
this.instigator = instigator;
this.damage = damage;
this.target = null;
isHoming = false;
}
public GameObject GetInstigator()
{
return instigator;
}
public float GetSpeed()
{
return speed;
}
public void SetSpeed(float speed)
{
this.speed = speed;
}
public float GetAlternativeSpeedVariable()
{
return alternativeSpeedVariable;
}
public float GetDamage()
{
return damage;
}
public Vector3 GetTargetPoint()
{
return targetPoint;
}
}
}
BUT MY REALLY, REALLY, REALLY, REALLY BIG PROBLEM, IS THAT I DID NOT COUNT FOR ANY OF THESE WHEN DEVELOPING MY RANGER AIMING STATE