RPG => 3RD PERSON arrow doesn't instantiate

Hello.

I have a problem with bow attack.

It doesn’t instantiate any Arrow.

Here is my bow

I’ve a projectile associated to it:

It tag is Projectiles

here are my Fighter, weaponConfig and Projectile script:

Fighter

using UnityEngine;
using RPG.Movement;
using RPG.Core;
using GameDevTV.Saving;
using RPG.Attributes;
using RPG.Stats;
using System.Collections.Generic;
using GameDevTV.Utils;
using GameDevTV.Inventories;
using System;
using Newtonsoft.Json.Linq;

namespace RPG.Combat
{
    public class Fighter : MonoBehaviour, IAction
{

    [SerializeField] private float timeBetweenAttacks = 1f;
    [SerializeField] private Transform rightHandTransform = null;
    [SerializeField] private Transform leftHandTransform = null;
    [SerializeField] private WeaponConfig defaultWeapon = null;
    [SerializeField] float autoAttackRange = 4f;

    public event System.Action<WeaponConfig> OnWeaponChanged;
    Health target;//On accède au script Health du porteur

    private Equipment equipment;
    private ForceReceiver forceReceiver;//Ajout 3rd person chap 20
    private BaseStats baseStats;
    private bool isPlayer;

    float timeSinceLastAttack = Mathf.Infinity;
    WeaponConfig currentWeaponConfig;
    LazyValue<Weapon> currentWeapon;

    private Attack currentAttack;//Ajout 3RDPerson

        /// <summary>
        /// Obtenir l'animation spécifiée par index et définir l'attaque actuelle.
        /// </summary>
        /// <param name="attack"></param>
        /// <returns></returns>
        public Attack GetCurrentAttack(int attack)
        {
            if (currentWeaponConfig == null || currentWeaponConfig.Attacks.Length==0) return null;
            if (attack < 0 || attack >= currentWeaponConfig.Attacks.Length)
            {
                attack = 0;
            }
            currentAttack =  currentWeaponConfig.Attacks[attack];
            return currentAttack;
        }

        /// <summary>
        /// Définit l'attaque actuelle. Utile lorsque l'attaque contient une attaque combo.
        /// </summary>
        /// <param name="attack"></param>
        public void SetCurrentAttack(Attack attack)
        {
            currentAttack = attack;
        }

        private void Awake()
        {
            baseStats = GetComponent<BaseStats>();//Ajout 3rd Person chap 21
            currentWeaponConfig = defaultWeapon;
            currentWeapon = new LazyValue<Weapon>(SetupDefaultWeapon);
            equipment = GetComponent<Equipment>();
            if (equipment)
            {
                equipment.equipmentUpdated += UpdateWeapon;
            }
            forceReceiver = GetComponent<ForceReceiver>();//Ajout 3rd Person
            isPlayer = gameObject.CompareTag("Player");//Ajout 3rd Person chap 21
        }

        private Weapon SetupDefaultWeapon()
        {
            return AttachWeapon(defaultWeapon);
        }

        private void Start()
        {
            currentWeapon.ForceInit();
        }

        private void Update()
        {
            if (isPlayer) return;//Ajout 3rd Person chap 21
            timeSinceLastAttack += Time.deltaTime; //Le temps qui s'écoule depuis la dernière attaque 

            if (target == null) return;//S'il n'y a pas de cible cliquée on ne lit pas la suite de l'update
            if (target.IsDead())
            {
                target = FindNewTargetInRange();
                if(target == null) return;
            }
            
            if(!GetIsInRange(target.transform))//S'il n'est pas à portée c'est qu'on a quand même cliqué sur un enemi dont target n'est pas null, donc on avance vers l'ennemi
            {
                GetComponent<Mover>().MoveTo(target.transform.position, 1f);//On utilise la méthode MoveTo de Mover avec comme destination la target à la vitesse max.
            }
            else//sinon on arrète le mouvement et on attaque.
            {
                GetComponent<Mover>().Cancel();
                AttackBehavior();
            }
        }
        public void EquipWeapon(WeaponConfig weapon)
        {//if(defaultWeapon == null) return;//Si on n'a pas d'arme équipée on ne fait rien d'autre.
            currentWeaponConfig = weapon;
            currentWeapon.value = AttachWeapon(weapon);
        }
        private void UpdateWeapon()//Méthode pour mettre à jour l'arme depuis l'inventaire
        {
            //Récuperer l'arme depuis l'équipement
            var weapon = equipment.GetItemInSlot(EquipLocation.Weapon) as WeaponConfig;
            if (weapon == null)//Lancer le WeaponConfig
            {
                EquipWeapon(defaultWeapon);
            }
            else
            {
                EquipWeapon(weapon);
            }
            //Equiper l'arme
            //Que faire s'il n'y a pas d'arme équipée?
        }
        private Weapon AttachWeapon(WeaponConfig weapon)//Méthode pour récupérer l'arme équipée
        {
            Animator animator = GetComponent<Animator>();//On récupère l'animator de l'objet qui porte le script.
            return weapon.Spawn(rightHandTransform, leftHandTransform, animator);//On fait apparaitre l'arme sélectionnée et l'animation associée.
        }

        public Health GetTarget()//Ajout 3RD Person
        {
            return target;
        }
        public Transform GetHandTransform(bool isRightHand)
        {
            if (isRightHand)
            {
                return rightHandTransform;
            }
            else
            {
                return leftHandTransform;
            }
        }
        private void AttackBehavior()
        {
            transform.LookAt(target.transform);
            if(timeSinceLastAttack > timeBetweenAttacks)
            {
                // Cela déclenchera l'événement Hit().
                TriggerAttack();
                timeSinceLastAttack = 0;
            }
        }
        private Health FindNewTargetInRange()
        {
            Health best = null;
            float bestDistance = Mathf.Infinity;//Tout ce qui est infèrieur à l'infini
            foreach(var candidate in FindAllTargetsInRange())
            {
                float candidateDistance = Vector3.Distance(transform.position, candidate.transform.position);//On mesure la distance entre la cible et le joeuur
                if(candidateDistance < bestDistance)
                {
                    best = candidate;
                    bestDistance = candidateDistance;
                }
            }
            return best;
        }

        private IEnumerable<Health> FindAllTargetsInRange()
        {
            RaycastHit[] raycastHits = Physics.SphereCastAll(transform.position,autoAttackRange, Vector3.up );
            foreach(var hit in raycastHits)
            {
                Health health = hit.transform.GetComponent<Health>();
                if(health == null) continue;//Si l'objet dans la sphere de cast n'a ps de script Health, on continue la recherche
                if(health.IsDead()) continue;//Si le PNJ est mort, on continue la recherche
                if(health.gameObject == gameObject) continue;//On s'exclu de la recherche.
                yield return health;
            }
        }

        private void TriggerAttack()
        {
            GetComponent<Animator>().ResetTrigger("stopAttack");//Pour éviter le bug lorsqu'on sort du combat où le trigget stopAttack reste actif et qui fait en sorte que lors de la prochaine attaque le joueur semble faire une pause avant de frapper.
            GetComponent<Animator>().SetTrigger("attack");
        }

        //méthode uniquement pour event de l'animator
        private void Hit()
        {
            //Si on sort du combat avant la frame qui déclenche l'event hit, alors on ne lit pas la suite du code. Evite le message d'erreur NullReferenceException
            if(target == null) {return;}

            float damage = GetComponent<BaseStats>().GetStat(Stat.Damage);
            BaseStats targetBaseStats = target.GetComponent<BaseStats>();
            if(targetBaseStats != null)
            {            
                float defence = targetBaseStats.GetStat(Stat.Defence);
                Debug.Log($"{name} hit {target.name} with {damage} attack.  Target's defense = {defence}");
                Debug.Log($"{damage} /= 1 + {defence}/{damage} = {damage / 1 + defence / damage}");
                damage /= 1 + defence / damage;
            }

            if(currentWeapon.value != null)
            {
                currentWeapon.value.OnHit();
            }
            
            if(currentWeaponConfig.HasProjectile())//Si l'arme actuelle est une arme à munitions
            {
                currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, target, gameObject, damage);
            }else{
                target.TakeDamage(gameObject, damage);//target étant relié directement au script Healt, on lance la méthode TakeDamage à chaque event hit.
            }//gameObject represente la personne qui inflige les dégats.       
        }
        /*private void TryApplyHitForce(Collider other, Vector3 position)
        {
            other.GetComponent<ForceReceiver>().AddForce((other.transform.position - position).normalized * currentAttack.HitForce);
        }*/
        void Shoot()//Ajout 3rd person chap 40 projectiles, remplace méthode du dessous du même nom
        {
            if (!currentWeaponConfig.HasProjectile()) return;
            
            if (TryGetComponent(out ITargetProvider targetProvider))
            {
                float damage = GetComponent<BaseStats>().GetStat(Stat.Damage);
                GameObject targetObject = targetProvider.GetTarget();
                if (targetObject != null)
                {
                    currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, targetObject.GetComponent<Health>(), gameObject, damage);
                }
                else
                {
                    currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, gameObject, damage);
                }
            }
        }


        /*private void Shoot()
        {
            Hit();
        }*/
        void TryHit(int slot)//Ajout 3rdPerson chap 19
        {
            if (currentAttack == null) return;//Ajout chap 21
            Vector3 transformPoint;
            float damageRadius = .5f;//Ajout Chap 21
            switch (slot)
            {
                case 0:
                    transformPoint = currentWeapon.value.DamagePoint;
                    damageRadius = currentWeapon.value.DamageRadius;
                    break;
                case 1:
                    transformPoint = rightHandTransform.position;
                    break;
                case 2:
                    transformPoint = leftHandTransform.position;
                    break;
                default:
                    transformPoint = rightHandTransform.position;
                    break;
            }
            Debug.Log($"Attacking with slot {slot}, position {transformPoint}");
            foreach (Collider other in Physics.OverlapSphere(transformPoint, damageRadius))//Ajout boucle complète Chap 21
            {
                if (other.gameObject == gameObject) continue;
                if (other.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead())//Ajout 3RD Person Chap 34 de la condition !otherHealth.IsDead()
                {
                    Debug.Log($"Hitting {otherHealth.gameObject}");
                    float damage = baseStats.GetStat(Stat.Damage);
                    damage *= currentAttack.DamageModifier; //Allows for varying damage based on attack style.
                    if (other.TryGetComponent(out BaseStats otherBaseStats))
                    {
                        float defence = otherBaseStats.GetStat(Stat.Defence);
                        damage /= 1 + defence / damage;
                    }
                    otherHealth.TakeDamage(gameObject, damage);
                    TryApplyHitForce(other, transform.position);//Ajout Chap 35 Damage knockback
                }
            }
        }
        private void TryApplyHitForce(Collider other, Vector3 transformPoint)//Ajout chap 35
        {
            other.GetComponent<ForceReceiver>().AddForce((other.transform.position - transformPoint).normalized * currentAttack.HitForce, true);
        }
        void ApplyAttackForce()
        {
            if (!forceReceiver) return;
            forceReceiver.AddForce(transform.forward * currentAttack.AttackForce);
        }

        private bool GetIsInRange(Transform targetTransform)//On défini la cible et non plus la distance via le navMesh 
        {
            return Vector3.Distance(transform.position, targetTransform.position) < currentWeaponConfig.GetRange();
        }
        
        public bool CanAttack(GameObject combatTarget)
        {
            if (combatTarget == null) {return false;}
            if (!GetComponent<Mover>().CanMoveTo(combatTarget.transform.position) &&
                !GetIsInRange(combatTarget.transform)) 
            {
                return false;
            }//Pour empecher le deplacement vers une cible au dela de la portée de mouvement max definie.
            Health targetToTest = combatTarget.GetComponent<Health>();
            return targetToTest != null && !targetToTest.IsDead();//REnvoyer l'info qu'il y a bien une cible et qu'elle n'est pas morte
        }
        public void Attack(GameObject combatTarget)
        {
            if (isPlayer) return;
            GetComponent<ActionScheduler>().StartAction(this);//Lorsqu'on attaque on informe le script ActionScheduler que l'action en cours est attaque.
            target = combatTarget.GetComponent<Health>();//La cible est le transform de la combatTarget.
        }

        public void Cancel()
        {
            if (isPlayer) return;
            StopAttack();//La méthode Cancel désactive l'attaque.
            target = null;
            GetComponent<Mover>().Cancel();//La méthode Cancel déscative aussi le mouvement.
        }

        private void StopAttack()
        {
            GetComponent<Animator>().ResetTrigger("attack");//Pour remettre à 0 le trigger attaque lors de l'annulation de l'attaque.
            GetComponent<Animator>().SetTrigger("stopAttack");
        }
        public float GetAttackingRange()
        {
            return currentWeaponConfig.GetRange();
        }
        /*public object CaptureState()
        {
            return currentWeaponConfig.name;
        }

        public void RestoreState(object state)
        {
            string weaponName = (string)state;
            WeaponConfig weapon = UnityEngine.Resources.Load<WeaponConfig>(weaponName);
            EquipWeapon(weapon);
        }
        public JToken CaptureAsJToken()
        {
            return JToken.FromObject(currentWeaponConfig.name);
        }

        public void RestoreFromJToken(JToken state)
        {
            string weaponName = state.ToObject<string>();
            WeaponConfig weapon = UnityEngine.Resources.Load<WeaponConfig>(weaponName);
            EquipWeapon(weapon);
        }*/

    }
}

WeaponConfig

using System;
using UnityEngine;
using RPG.Attributes;
using GameDevTV.Inventories;
using RPG.Stats;
using System.Collections.Generic;
using UnityEditor;


namespace RPG.Combat
{
    [CreateAssetMenu(fileName = "weapon", menuName = "RPG Weapons/Make new Weapon", order = 0)] 
    public class WeaponConfig:EquipableItem, IModifierProvider
    {
        [SerializeField] private int animatorWeaponType;//Ajout chap 31
        //[SerializeField] private AnimatorOverrideController animatorOverride = null;//Suppression chap 31 RPG=>3RD
        [SerializeField] private Weapon equippedPrefab = null;
        [SerializeField] private float weaponDamage = 5f;
        [SerializeField] private float percentageBonus = 0f;
        [SerializeField] private float weaponRange = 2f;
        [SerializeField] private float targetingRange = 10f;//Ajout Oublie dans le github
        [SerializeField] private bool isRightHanded = true;
        [SerializeField] private Projectile projectile = null;
        [field: SerializeField] public Attack[] Attacks { get; private set; }//Ajout pour les 3rd Person settings
        const string weaponName = "Weapon"; //cette constante est créée en string car on va utiliser FindObject qui cherche des strings.
        public int AnimatorWeaponType => animatorWeaponType;//Ajout chap 31
        public Weapon Spawn(Transform rightHand, Transform leftHand, Animator animator)
        {
            DestroyOldWeapon(rightHand, leftHand);

            Weapon weapon = null;

            if (equippedPrefab != null)
            {
                Transform handTransform = GetTransform(rightHand, leftHand);
                weapon = Instantiate(equippedPrefab, handTransform);//On instancie l'arme dans la main adéquate
                weapon.gameObject.name = weaponName;
            }
            //Retrait Chap 31
            /*if (!animator.gameObject.CompareTag("Player"))//Ajout 3rdPerson
            {
                var overrideController = animator.runtimeAnimatorController as AnimatorOverrideController;//Ajout 3rdPerson
                if (animatorOverride != null)
                {
                    animator.runtimeAnimatorController = animatorOverride;//On remplace l'animation d'avant par celle correspondant à l'arme équipée
                }
                else if (overrideController != null)//Ajout 3rdPerson
                {
                    animator.runtimeAnimatorController = overrideController.runtimeAnimatorController;
                }
            }*/
            
            animator.SetFloat("WeaponType", animatorWeaponType);
            
            return weapon;
        }
        private void DestroyOldWeapon(Transform rightHand, Transform leftHand)
        {
            Transform oldWeapon = rightHand.Find(weaponName);
            if (oldWeapon == null)
            {
                oldWeapon = leftHand.Find(weaponName);
            }
            if (oldWeapon == null) return;

            oldWeapon.name = "DESTROYING";
            Destroy(oldWeapon.gameObject);
        }
        private Transform GetTransform(Transform rightHand, Transform leftHand)//Méthode pour récupérer la main d'origine de l'arme ou du projectile
        {
            Transform handTransform;
            if (isRightHanded) handTransform = rightHand;
            else handTransform = leftHand;
            return handTransform;
        }

        public bool HasProjectile()
        {
            return projectile != null;//La booléenne sert à vérifier que le porteur du script a encore des projectiles.
        }

        public void LaunchProjectile(Transform rightHand, Transform leftHand, Health target, GameObject instigator, float calculatedDamage)//la méthode de projection d'objet à besoin de connaitre son point d'origine et d'arrivée (le porteur du script Health cliqué)
        {
            Debug.Log($"Launch Projectile ({target}), instigator = {instigator}");
            //On fait apparaitre un prefab (instance) du projectile dans la bonne main du personnage
            Projectile projectileInstance = Instantiate(projectile, GetTransform(rightHand, leftHand).position, Quaternion.identity);
            //On lui assigne la cible:
            projectileInstance.SetTarget(target,instigator, calculatedDamage);
        }
        //Ajout GitHub chap 40 sur les projectiles
        public void LaunchProjectile(Transform rightHandTransform, Transform leftHandTransform, GameObject instigator, float damage)
        {
            Debug.Log($"LaunchProjectile(noTarget), instigator = {instigator}");
            Transform correctTransform = GetTransform(rightHandTransform, leftHandTransform);
            Projectile projectileInstance = Instantiate(projectile, correctTransform.position, Quaternion.identity);
            projectileInstance.transform.forward = instigator.transform.forward;
            projectileInstance.SetupNoTarget(instigator, damage);
        }

        public float GetDamage()
        {
            return weaponDamage;
        }
        public float GetPercentageBonus()
        {
            return percentageBonus;
        }
        public float GetRange()
        {
            return weaponRange;
        }
        public IEnumerable<float> GetAdditiveModifiers(Stat stat)
        {
            if(stat == Stat.Damage)
            {
                yield return weaponDamage;
            }
        }
        
        public IEnumerable<float> GetPercentageModifiers(Stat stat)
        {
            
            if(stat == Stat.Damage)
            {
                yield return percentageBonus;
            }
        }

        public float GetTargetingRange()
        {
            return targetingRange;
        }


        #region InventoryItemEditor Additions

        public override string GetDescription()
        {
            string result = projectile ? "Ranged Weapon" : "Melee Weapon";
            result += $"\n\n{GetRawDescription()}\n";
            result += $"\nRange {weaponRange} meters";
            result += $"\nBase Damage {weaponDamage} points";
            if ((int)percentageBonus != 0)
            {
                string bonus = percentageBonus > 0 ? "<color=#8888ff>bonus</color>" : "<color=#ff8888>penalty</color>";
                result += $"\n{(int)percentageBonus} percent {bonus} to attack.";
            }
            return result;
        }

#if UNITY_EDITOR

        void OnValidate()
        {
            if (GetAllowedEquipLocation() != EquipLocation.Weapon)
            {
                SetAllowedEquipLocation(EquipLocation.Weapon);
            }
        }

        void SetWeaponRange(float newWeaponRange)
        {
            if (FloatEquals(weaponRange, newWeaponRange)) return;
            SetUndo("Set Weapon Range");
            weaponRange = newWeaponRange;
            Dirty();
        }

        void SetWeaponDamage(float newWeaponDamage)
        {
            if (FloatEquals(weaponDamage, newWeaponDamage)) return;
            SetUndo("Set Weapon Damage");
            weaponDamage = newWeaponDamage;
            Dirty();
        }

        void SetPercentageBonus(float newPercentageBonus)
        {
            if (FloatEquals(percentageBonus, newPercentageBonus)) return;
            SetUndo("Set Percentage Bonus");
            percentageBonus = newPercentageBonus;
            Dirty();
        }

        void SetIsRightHanded(bool newRightHanded)
        {
            if (isRightHanded == newRightHanded) return;
            SetUndo(newRightHanded?"Set as Right Handed":"Set as Left Handed");
            isRightHanded = newRightHanded;
            Dirty();
        }

        /*void SetAnimatorOverride(AnimatorOverrideController newOverride)
        {
            if (newOverride == animatorOverride) return;
            SetUndo("Change AnimatorOverride");
            animatorOverride = newOverride;
            Dirty();
        }*/

        void SetEquippedPrefab(GameObject potentialnewWeapon)
        {
            if (!potentialnewWeapon)
            {
                SetUndo("No Equipped Prefab");
                equippedPrefab = null;
                Dirty();
                return;
            }
            if (!potentialnewWeapon.TryGetComponent(out Weapon newWeapon)) return;
            if (newWeapon == equippedPrefab) return;
            SetUndo("Set Equipped Prefab");
            equippedPrefab = newWeapon;
            Dirty();
        }

        void SetProjectile(GameObject potentialNewProjectile)
        {
            if (!potentialNewProjectile)
            {
                SetUndo("No Projectile");
                projectile = null;
                Dirty();
                return;
            }
            if (!potentialNewProjectile.TryGetComponent(out Projectile newProjectile))
            {
                return;
            }
            if (newProjectile == projectile) return;
            SetUndo("Set Projectile");
            projectile = newProjectile;
            Dirty();
        }

        public override bool IsLocationSelectable(Enum location)
        {
            EquipLocation candidate = (EquipLocation) location;
            return candidate == EquipLocation.Weapon;
        }

        bool drawWeaponConfigData = true;
        public override void DrawCustomInspector()
        {
            base.DrawCustomInspector();
            drawWeaponConfigData = EditorGUILayout.Foldout(drawWeaponConfigData, "Données de configuration de l'arme",foldoutStyle);
            if (!drawWeaponConfigData) return;
            EditorGUILayout.BeginVertical(contentStyle);
            //Trick to allow searching for the prefab using the . button instead of having to drag it in
            GameObject potentialPrefab = equippedPrefab?equippedPrefab.gameObject:null;
            SetEquippedPrefab((GameObject)EditorGUILayout.ObjectField("Prefab Équipé", potentialPrefab,typeof(GameObject), false));
            
            SetWeaponDamage(EditorGUILayout.Slider("Dégats de l'arme", weaponDamage, 0, 1000));
            SetWeaponRange(EditorGUILayout.Slider("Portée de l'arme", weaponRange, 1,40));
            SetPercentageBonus(EditorGUILayout.IntSlider("Pourcentage de Bonus", (int)percentageBonus, -10, 100));
            SetIsRightHanded(EditorGUILayout.Toggle("Main droite", isRightHanded));
            //SetAnimatorOverride((AnimatorOverrideController)EditorGUILayout.ObjectField("Outrepasser l'Animator", animatorOverride, typeof(AnimatorOverrideController), false));
            GameObject potentialProjectile = projectile ? projectile.gameObject : null;
            SetProjectile((GameObject)EditorGUILayout.ObjectField("Projectile", potentialProjectile, typeof(GameObject), false));            
            EditorGUILayout.EndVertical();
        }

#endif
#endregion

    }
}

And the Projectile script

using RPG.Attributes;
using RPG.Movement;
using UnityEngine;
using UnityEngine.Events;

namespace RPG.Combat
{
    public class Projectile : MonoBehaviour
{
    [SerializeField] private float speed = 1;
    [SerializeField] private float projectileDamage = 0;
    [SerializeField] private bool isHoming = true;//Tête chercheuse.
    [SerializeField] private GameObject hitEffect = null;
    [SerializeField] private float MaxLifeTime = 5;//Pour détruire les missiles qui ont manqués leur cible.
    [SerializeField] private GameObject[] destroyOnHit = null;//Création d'une liste d'objet qu'on souhaite détruire à l'impact.
    [SerializeField] private float lifeAfterImpact = 2f;
    [SerializeField] private UnityEvent onHit;
    
    Health target = null;
    Vector3 targetPoint;
    GameObject instigator = null;
    float damage = 0;
    void Start()//Modif 3rd person chap 40 ajout if(target)
    {
        if (target) transform.LookAt(GetAimLocation());//Dés le départ on vise la cible.Annule l'effet missile chercheur.
    }
    void Update()
    {
        if (target == null) return;
        if(target != null && isHoming && !target.IsDead())//Si le missile est à tête chercheuse, il va suivre la cible. sauf s'il est mort, il continuera son chemin
        {
            transform.LookAt(GetAimLocation());
        }
        transform.Translate(Vector3.forward * speed * Time.deltaTime);
    }
    public void SetTarget(Health target, GameObject instigator, float damage)
    {
        SetTarget(instigator, damage, target);
    }

    public void SetTarget(Vector3 targetPoint, GameObject instigator, float damage)
    {
        SetTarget(instigator, damage, null, targetPoint);
    }
    public void SetTarget(GameObject instigator, float damage, Health target=null, Vector3 targetPoint=default)
    {
        Debug.Log($"Arrow fired for {projectileDamage} damage");
        this.target = target; // La santé de la cible est celle du porteur de ce script ciblé.
        this.targetPoint = targetPoint;
        this.damage += damage; //Les dommages infligés par le porteur du script (le projectile) sont les dommages du projectile.
        this.instigator = instigator;
        
        Destroy(gameObject, MaxLifeTime);
    }
    private Vector3 GetAimLocation()
    {
        if(target ==null)//target cherchant un script Heal, si l'endroit pointé n'en a pas, l'endroit visé devient l'endroit pointé.
        {
            return targetPoint;
        }
            //BoxCollider targetBox =target.GetComponent<BoxCollider>();
            var targetCapsule = target.GetComponent<CharacterController>();
        if (targetCapsule == null)//était targetBox
            {
                return target.transform.position;
            }
        return target.transform.position + Vector3.up * targetCapsule.height /2;
    }

    private void OnTriggerEnter(Collider other)
    {
        //Retrait des 3 lignes suivantes suite à 3RD Person Chap 40 projectiles.
        //Health health= other.GetComponent<Health>();
        //if(target != null && health != target) return;//Si l'objet touché n'a pas de composant Health, on ne lit pas la suite du code.
        //if (health == null || health.IsDead()) return;//Si la cible est morte ou s'il n'y a pas de cible avec le script Health, la fleche continue son chemin.
        if (other.gameObject == instigator) return;
        if (other.TryGetComponent(out Health health))//On vérifie si la cible a un composant Health, si oui on lui inflige les dégâts
            {
                health.TakeDamage(instigator, damage);
            }
        //Ensuite on vérifies'il la cible à un composant ForceReceiver puis on applique la force dans la direction du projectile
        if (other.TryGetComponent(out ForceReceiver forceReceiver))//Ajout 3RD Person Chap 40 Projectiles
            {
                forceReceiver.AddForce(transform.forward * damage, true);
            }
            speed = 0;

            onHit.Invoke();

        //Création de l'efffet d'impact
        if(hitEffect != null)
        {
        Instantiate(hitEffect, GetAimLocation(), transform.rotation);
        }

        /*target.TakeDamage(instigator,projectileDamage);

        speed = 0.5f;//On arrete le projectile*/
        //Destruction à l'impact de la liste d'objet qu'on a choisi.
        foreach (GameObject toDestroy in destroyOnHit)
        {
            Destroy(toDestroy);
        }
        Destroy(gameObject , lifeAfterImpact);
    }
    //Ajout Github chapitre 40 projectiles
    public void SetupNoTarget(GameObject instigator, float damage)
    {
        this.instigator = instigator;
        this.damage = damage;
        this.target = null;
        isHoming = false;
    }

}
}

The bow spawn correctly and the shooting animation is the good one but no arrow spawns at all.

Any idea?

Do you need other information?

Here is my layer collision matrix

Thank you.

François

(and thank you for your patience :slight_smile: )

Hi François.

Nothing obvious jumps at me from the stuff you show.

I’m not sure what your development/course path is. Your title suggests you are transitioning from the RPG to the 3rd person combat/traversal stuff. Apparently you are working with code from the Inventory course, too, but shooting arrows was already done in RPG core/combat. So, did it work for you at some previous point? With which change did it stop working? If you are in the middle of a course, can you compare with the instructor’s project?

Is the setup with the animation, animator override etc. as in the course? Does the firing animation play? Does it contain the Shoot event (Hit should work too, I suppose)? Is the character prefab (in particular Fighter, Animator) set up as the instructor’s?

Can you check which of the relevant methods get executed (break points, debug log) and if that looks as expected? In particular, we’re interested in anything happening between Shoot/Hit and the LaunchProjectile methods.

Hello daberny.

Thanks for your feedback.

Yes i’m transitioning from RPG to 3RD person.

Yes i implemented the Brian’s inventory system in my project.

Attack with a bow worked fine before I turn to 3RD person transition.

As we replace the the freelookblendtree and the targetingblendtree during the course, It never worked.

I’m at the last lesson according transitioning to 3RD person.

The fire animation played, there is a Shoot event:

Note there is no TryHit like in Brian’s script.

Fighter and animator as set as Brian:

Mine:

Brian:

LaunchProjectile method never plays in my project.

I don’t have any idea of what happens…

Thank you.

François

I haven’t done the 3rd person transition myself, which limits my qualification to help, sorry. I also don’t know what TryHit does and whether it might play a role here.

I suppose you have to investigate the stuff around the Shoot event. Suggestions:

Verify that the Shoot event handling method is called, too, just to make sure the problem isn’t in the logic between that and LaunchProjectile.

Make modified copies of your player prefab for testing; check if:

  • the Shoot event works if triggered from the firing animation outside of a blend tree (though I am reading that people are having the opposite problem with blend trees - events are triggered more than they would like),
  • a different component on the same game object can receive and handle the Shoot event (make a simple new MonoBehavior just for this).

Hopefully results can help you pin the source of the problem…

Hello Daberny.

I progress.

I’m reading again the Brian’s Tuto, chapter 40 and 41 (the last) an I correct some missing by cross checking Brian an mine scripts.

Now enemy bowman trigger hit animation but I can’t see any arrow spawning.

A small littl “bug”, bowman doesn’t face me when he triggers the shoot :sweat_smile:

I don’t recieved any damage.

I’m still actually not able tu hit enemy with my bow.

But I progess lol.

François

Ok, so i’m debug step by step what happens.

I debugged the Shoot Method in Fighter Script:

        void Shoot()//Ajout 3rd person chap 40 projectiles, remplace méthode du dessous du même nom
        {
            Debug.Log("Shoot() Called : tentative de tir.");

            if (!currentWeaponConfig.HasProjectile())
            {
                Debug.Log("No projectiles configured for this weapon.");
                return;
            }

            if (TryGetComponent(out ITargetProvider targetProvider))
            {
                float damage = GetComponent<BaseStats>().GetStat(Stat.Damage);
                GameObject targetObject = targetProvider.GetTarget();

                if (targetObject != null)
                {
                    Debug.Log("Projectile launched towards the target : " + targetObject.name);
                    currentWeaponConfig.LaunchProjectile(
                        rightHandTransform,
                        leftHandTransform,
                        targetObject.GetComponent<Health>(),
                        gameObject,
                        damage
                    );
                }
                else
                {
                    Debug.Log("No target found: projectile launched in default direction.");
                    currentWeaponConfig.LaunchProjectile(
                        rightHandTransform,
                        leftHandTransform,
                        gameObject,
                        damage
                    );
                }
            }
        }

And in the console I can see that the bowman shoot me.

But he doesn’t face me and there is no arrow spawning:

I keep on debug test on the projectile script .

Edit, the weaponConfig debug:

My débug code:

        public void LaunchProjectile(Transform rightHand, Transform leftHand, Health target, GameObject instigator, float calculatedDamage)//la méthode de projection d'objet à besoin de connaitre son point d'origine et d'arrivée (le porteur du script Health cliqué)
        {
            Debug.Log($"LaunchProjectile(target={target}), instigator={instigator.name}");

            Transform spawnPoint = GetTransform(rightHand, leftHand);
            Projectile projectileInstance = Instantiate(projectile, spawnPoint.position, Quaternion.identity);

            Debug.Log($"Instantiated projectile (with target) : {projectileInstance.name} to the position {spawnPoint.position}");

            projectileInstance.SetTarget(target, instigator, calculatedDamage);
        }
        //Ajout GitHub chap 40 sur les projectiles
        public void LaunchProjectile(Transform rightHandTransform, Transform leftHandTransform, GameObject instigator, float damage)
        {
            Debug.Log($"LaunchProjectile(noTarget), instigator={instigator.name}");

            Transform spawnPoint = GetTransform(rightHandTransform, leftHandTransform);
            Projectile projectileInstance = Instantiate(projectile, spawnPoint.position, Quaternion.identity);

            Debug.Log($"Instantiated projectile (without target) : {projectileInstance.name} to the position {spawnPoint.position}");

            projectileInstance.transform.forward = instigator.transform.forward;
            projectileInstance.SetupNoTarget(instigator, damage);
        }

Very strange… and my player play animation shoot but is never seen in the console such as the bowman…

Erfff… I don’t understand what’s wrong lol.

Edit:

I crosschecked the Brian’s and mine EnemyAttackingState and noticed I forgot 2 line at the end of the Tick methode.

            FaceTarget(stateMachine.Player.transform.position, deltaTime);
            Move(deltaTime);

Now the Fighter face me when i Attack me.

But always no arrow spawning.

I notice 2 strange Behavior now.

My Bowman rush me before trowing invisible arrows. It looks he doesn’t care about w my weapon confing Range Attack set to 20:

After cross checked with the Brain Setup for is bow, i’ve too modify the Targetting range to 25 like him (before it was to 10).

But nothing change, he rush me at the begining.

Other thing, I put some Debug in the projectile OnTriggerEnter and there are some weird behavior.

The arrow doesn’t spawm bu hit the bowman some time and other stuff like ConversantFinder or ShopfindeR…

Edit:

In the Projectile script I added a line too spawning the arrow far away from the handd but nothing Looks changed:
Transform spawnPoint = GetTransform(rightHand, leftHand);

        Vector3 offset = spawnPoint.forward \* 1f; //1 m devant la main

        Projectile projectileInstance = Instantiate(projectile, spawnPoint.position, Quaternion.identity);

Schuuss

François

You are not using the offset in the second line.

From your logs, it becomes obvious that arrows are spawned. Maybe you just don’t see them because they hit something on the character that shoots them right after spawning. Since you are collapsing log messages, it is not obvious what happens in what order - seems both the player and at least one archer enemy are firing, so it’s not clear who’s arrow hits what. (To check that, disable the log collapsing and test with scenarios in which only one arrow is fired.) Possible that there is another reason why your arrow is not visible to you, but I suspect it just hits something on the game object that fires it instead.

I just looked at how it was set up for my project (matching original Core course, I think). Nothing smart about the Collision matrix, and the OnTriggerEnter method in the Projectile script does nothing except if the hit gameObject is the target gameObject - probably not the most efficient (method gets triggered uselessly and can’t react properly to hitting obstacles), but at least prevents effects such as yours. I see that you have had multiple variants of the if conditions at the start of this method, and apparently the current version doesn’t do what you want it to.

Hello Daberny.

Thanks for your reply.

You’re right.

When I don’t collapse the log it appears the arrow launch by the enemy(targeter) hits himself:

I must admit this happens when I lightly change the initial Brian’’s code which check if the arrows collide the instigator, we return.

I tested by replacing in the OnTriggerEnter Merthod in Prjectile Script:

if (other.gameObject == instigator) return;

by

if (other.transform.root.gameObject == instigator)
            {
                return;
            }

With this code, the Bowman rush me and I can see his life bar and he’s hit by his arrow and lost HP.

When he rushes me and launch the invisible arrow he can damage me.

But the most part of the time he hit himself loosing life and finishing by dying.

If I let the original code:

if (other.gameObject == instigator) return;

at the same place, the start of the OnTriggerEnter method,

The enemy doesn’t rush me, but his life bar desappear and he dosen’t seem to lose life

The log wrotte 20 damage done and he is the damaged object but his life doesn’t seem to reduce.

It’s strange it’s not the same damage value… It wrote that arrow hit for 20 but player is hitting really for 22 like lol :sweat_smile:
The code line react correctly without damaging the targeter but the only moment Bowman damaged me is when I rush him…
Erfff…. :slight_smile:

When I rush him, i’m hitting by the invisible arrow and loose… 22 hitpoints…

Not 20…

My poisonned arrow hit set to hit 20 hit point, not 22 lol…

At work, I’m known for having computer and software problems that nobody else encounters.
It looks like it’s the same in coding :sweat_smile: :joy:
Thanks for your patience.
François

Edit:

Don’t take care about my hit point, it’s normal, my bowman is set to level 10 and must hit for … 22 hit point :slight_smile:

Don’t care too about lifeBar… I remember that it spawns only when the enemy is hitten… It work nicely when I hit with a staff the enemy.

But nothing still happens with my bow…

Edit 2:

I disabled the arrow destruction to see what happen.

The Arrow spawns well

But it isn’t throw…

This confirms, I think, that the arrow was destroyed instantly.

I keep on investigate step by step :sweat_smile: , I’m sure it’s a really stupid mistake. But wich one???

Can you paste in your full Projectile.cs as it stands now?

Hello Brian !

So happy to read you again :slight_smile:

here is my actual Projectile Script:

using RPG.Attributes;
using RPG.Movement;
using UnityEngine;
using UnityEngine.Events;
//Crosscheck fait le 2026-05-08
namespace RPG.Combat
{
    public class Projectile : MonoBehaviour
{
    [SerializeField] private float speed = 1;
    [SerializeField] private float projectileDamage = 0;
    [SerializeField] private bool isHoming = true;//Tête chercheuse.
    [SerializeField] private GameObject hitEffect = null;
    [SerializeField] private float MaxLifeTime = 10;//Pour détruire les missiles qui ont manqués leur cible.
    [SerializeField] private GameObject[] destroyOnHit = null;//Création d'une liste d'objet qu'on souhaite détruire à l'impact.
    [SerializeField] private float lifeAfterImpact = 2f;
    [SerializeField] private UnityEvent onHit;
    
    Health target = null;
    Vector3 targetPoint;
    GameObject instigator = null;
    float damage = 0;

    void Start()//Modif 3rd person chap 40 ajout if(target)
    {
        if (target) transform.LookAt(GetAimLocation());//fait pour que le projectile en cas de non cioble parte dans la direction pointée et non à l'origine.
    }

    void Update()
    {
        if(target != null && isHoming && !target.IsDead())//Si le missile est à tête chercheuse, il va suivre la cible. sauf s'il est mort, il continuera son chemin
        {
            transform.LookAt(GetAimLocation());
        }
        transform.Translate(Vector3.forward * speed * Time.deltaTime);
    }
    
    public void SetTarget(Health target, GameObject instigator, float damage)
        {
            SetTarget(instigator, damage, target);
        }

    public void SetTarget(Vector3 targetPoint, GameObject instigator, float damage)
    {
        SetTarget(instigator, damage, null, targetPoint);
    }
    public void SetTarget(GameObject instigator, float damage, Health target=null, Vector3 targetPoint=default)
    {
        Debug.Log($"Arrow fired for {projectileDamage} damage");
        this.target = target; // La santé de la cible est celle du porteur de ce script ciblé.
        this.targetPoint = targetPoint;
        this.damage = damage; //Les dommages infligés par le porteur du script (le projectile) sont les dommages du projectile.2026-05-08 retrait du +
        this.instigator = instigator;
        
        Destroy(gameObject, MaxLifeTime);
    }
    private Vector3 GetAimLocation()
    {
        if(target ==null)//target cherchant un script Heal, si l'endroit pointé n'en a pas, l'endroit visé devient l'endroit pointé.
        {
            return targetPoint;
        }
            //BoxCollider targetBox =target.GetComponent<BoxCollider>();
            var targetCapsule = target.GetComponent<CharacterController>();
        if (targetCapsule == null)//était targetBox
            {
                return target.transform.position;
            }
        return target.transform.position + Vector3.up * targetCapsule.height /2;
    }

    private void OnTriggerEnter(Collider other)
    {
        
        if (other.gameObject == instigator) return;
        Debug.Log("Projectile collided with: " + other.name);

        if (other.TryGetComponent(out Health health))//On vérifie si la cible a un composant Health, si oui on lui inflige les dégâts
            {
                health.TakeDamage(instigator, damage);
            }
        //Ensuite on vérifies'il la cible à un composant ForceReceiver puis on applique la force dans la direction du projectile
        if (other.TryGetComponent(out ForceReceiver forceReceiver))//Ajout 3RD Person Chap 40 Projectiles
            {
                forceReceiver.AddForce(transform.forward * damage, true);
            }
            speed = 0;

            onHit.Invoke();

        //Création de l'efffet d'impact
        if(hitEffect != null)
        {
        Instantiate(hitEffect, GetAimLocation(), transform.rotation);
        }
        //Destruction à l'impact de la liste d'objet qu'on a choisi.
        foreach (GameObject toDestroy in destroyOnHit)
        {
            Destroy(toDestroy);
        }

        Destroy(gameObject , lifeAfterImpact);
    }
    //Ajout Github chapitre 40 projectiles pour le cas où il n'y ait pas de cible
    public void SetupNoTarget(GameObject instigator, float damage)
    {
        this.instigator = instigator;
        this.damage = damage;
        this.target = null;
        isHoming = false;//Si le projectile était à tête chercheuse, on le rend classique, droit devant.
    }

}
}
Thanks for the Help.
François

After a thorough re-read, it appears to be hitting the Targeter, which is always present on the Player…
I’d initially discounted this because the Targeter should be a RangeFinder, which you’ve matrixed out…
Check your Targeter and make sure it’s layer is set to RangeFinder

Hello Brian, thnaks for your feedback.

My targeter in player wasn’t set to Ranger Finder.

Now the enemy’s arrow spawn:

but doesn’t hurt me.

It destroyed around this sphere:

It’s my pickup Finder…

But I decreased it from 2 meters to 0.5meters and it’s not that. the arrow unspawns at the same place…
And my player is still unspwaning any arrows…

I watched your porject and your Targeter in the player isn’t st to RangeFinder but it works fine for you.

Console says me arrow it ConversantFinder and ShopFinder but not my littlest PickupFinder…

I checked it and it layer is on default, like you.

Sorry :slight_smile:

François