using UnityEngine; // Rifle held by the local player's humanoid (Y Bot): the AK47 model from Resources (falls back // to a primitive stand-in if it's missing), posed in // front of the right shoulder and pointed at whatever the crosshair is over, with both hands // pinned to it via Animator IK (needs "IK Pass" on the animator layer). Also owns the // procedural weapon animations: fire kick, muzzle flash, and the reload (mag drop / insert). // Runs after CharacterMovement so it reads the camera pose for this frame. [DefaultExecutionOrder(100)] [RequireComponent(typeof(Animator))] public class WeaponRig : MonoBehaviour { const string ModelPath = "Weapons/AK47"; public Vector3 shoulderOffset = new Vector3(0.18f, 1.45f, 0f); // Where the pistol grip (the gun's pivot) sits relative to the shoulder, in aim space. // Kept close so the Y Bot's left arm can reach the handguard. public Vector3 holdOffset = new Vector3(-0.02f, -0.12f, 0.24f); public Transform Muzzle { get; private set; } Animator animator; Transform gun; Transform rightGrip; Transform leftGrip; Transform magazine; Vector3 magazineRestPos; Transform flash; Light flashLight; Vector3 aimPoint; float kickBack; float kickPitch; float flashUntil; float equipProgress = 1f; bool equipping; float equipStart; float equipDuration; bool reloading; float reloadStart; float reloadDuration; bool magDropped; void Awake() { animator = GetComponent(); BuildGun(); } void OnDestroy() { if (gun != null) Destroy(gun.gameObject); } // Gun root pivot is the pistol grip (right hand); forward +Z, up +Y. void BuildGun() { gun = new GameObject("Rifle").transform; gun.SetParent(transform, false); // Real model (Quaternius CC0 AK47, see Resources/Weapons/AK47_LICENSE.txt), else a primitive stand-in. var model = Resources.Load(ModelPath); if (model != null) { var instance = Instantiate(model, gun, false).transform; instance.name = model.name; foreach (var c in instance.GetComponentsInChildren()) Destroy(c); Muzzle = instance.Find("Muzzle"); rightGrip = instance.Find("RightHandGrip"); leftGrip = instance.Find("LeftHandGrip"); magazine = instance.Find("Magazine"); } if (Muzzle == null || rightGrip == null || leftGrip == null || magazine == null) { if (model != null) Debug.LogWarning($"[WeaponRig] '{ModelPath}' is missing Muzzle/RightHandGrip/LeftHandGrip/Magazine; using the primitive rifle."); foreach (Transform child in gun) Destroy(child.gameObject); BuildPrimitiveGun(); } magazineRestPos = magazine.localPosition; flash = ShootingFx.CreateVisual(PrimitiveType.Sphere, "MuzzleFlash", Muzzle, new Vector3(0f, 0f, 0.06f), new Vector3(0.09f, 0.09f, 0.2f), ShootingFx.GetMaterial(new Color(1f, 0.7f, 0.25f), 6f)).transform; flash.gameObject.SetActive(false); flashLight = new GameObject("MuzzleLight").AddComponent(); flashLight.transform.SetParent(Muzzle, false); flashLight.type = LightType.Point; flashLight.color = new Color(1f, 0.75f, 0.4f); flashLight.range = 4f; flashLight.intensity = 1.5f; flashLight.enabled = false; } void BuildPrimitiveGun() { var dark = ShootingFx.GetMaterial(new Color(0.12f, 0.12f, 0.14f)); var metal = ShootingFx.GetMaterial(new Color(0.32f, 0.33f, 0.36f)); var accent = ShootingFx.GetMaterial(new Color(0.95f, 0.3f, 0.25f)); // Parts are laid out around the receiver; offset them so the grip sits at the root pivot. var body = new GameObject("PrimitiveRifle").transform; body.SetParent(gun, false); body.localPosition = new Vector3(-0.01f, 0.09f, 0.05f); ShootingFx.CreateVisual(PrimitiveType.Cube, "Receiver", body, new Vector3(0f, 0f, 0.05f), new Vector3(0.07f, 0.1f, 0.42f), dark); ShootingFx.CreateVisual(PrimitiveType.Cube, "Handguard", body, new Vector3(0f, -0.005f, 0.33f), new Vector3(0.075f, 0.085f, 0.2f), metal); ShootingFx.CreateVisual(PrimitiveType.Cylinder, "Barrel", body, new Vector3(0f, 0.01f, 0.5f), new Vector3(0.025f, 0.13f, 0.025f), dark, new Vector3(90f, 0f, 0f)); ShootingFx.CreateVisual(PrimitiveType.Cube, "Stock", body, new Vector3(0f, -0.025f, -0.26f), new Vector3(0.055f, 0.11f, 0.22f), metal); ShootingFx.CreateVisual(PrimitiveType.Cube, "Grip", body, new Vector3(0f, -0.1f, -0.06f), new Vector3(0.045f, 0.12f, 0.05f), dark, new Vector3(15f, 0f, 0f)); ShootingFx.CreateVisual(PrimitiveType.Cube, "Sight", body, new Vector3(0f, 0.07f, 0.04f), new Vector3(0.03f, 0.04f, 0.09f), dark); ShootingFx.CreateVisual(PrimitiveType.Cube, "SightDot", body, new Vector3(0f, 0.08f, 0.09f), Vector3.one * 0.012f, ShootingFx.GetMaterial(Color.red, 3f)); magazine = ShootingFx.CreateVisual(PrimitiveType.Cube, "Magazine", body, new Vector3(0f, -0.12f, 0.12f), new Vector3(0.045f, 0.16f, 0.08f), accent, new Vector3(-12f, 0f, 0f)).transform; Muzzle = new GameObject("Muzzle").transform; Muzzle.SetParent(body, false); Muzzle.localPosition = new Vector3(0f, 0.01f, 0.64f); rightGrip = new GameObject("RightHandGrip").transform; rightGrip.SetParent(body, false); rightGrip.localPosition = new Vector3(0.01f, -0.09f, -0.05f); leftGrip = new GameObject("LeftHandGrip").transform; leftGrip.SetParent(body, false); leftGrip.localPosition = new Vector3(-0.01f, -0.06f, 0.32f); } public void SetAimPoint(Vector3 point) => aimPoint = point; public void PlayFire() { kickBack = Mathf.Min(kickBack + 0.05f, 0.09f); kickPitch = Mathf.Min(kickPitch + 5f, 12f); flashUntil = Time.time + 0.04f; flash.localRotation = Quaternion.Euler(0f, 0f, Random.Range(0f, 360f)); flash.localScale = new Vector3(0.09f, 0.09f, 0.2f) * Random.Range(0.8f, 1.25f); } // Holds the rifle at low-ready until PlayEquip() raises it (used by the intro cinematic). public void SetLowered() { equipping = false; equipProgress = 0f; } public void PlayEquip(float duration) { equipping = true; equipStart = Time.time; equipDuration = Mathf.Max(0.01f, duration); } static float EaseOutBack(float x) { const float c1 = 1.70158f; const float c3 = c1 + 1f; return 1f + c3 * Mathf.Pow(x - 1f, 3f) + c1 * Mathf.Pow(x - 1f, 2f); } public void PlayReload(float duration) { reloading = true; reloadStart = Time.time; reloadDuration = duration; magDropped = false; } void LateUpdate() { float dt = Time.deltaTime; kickBack = Mathf.Lerp(kickBack, 0f, 1f - Mathf.Exp(-18f * dt)); kickPitch = Mathf.Lerp(kickPitch, 0f, 1f - Mathf.Exp(-14f * dt)); bool flashing = Time.time < flashUntil; flash.gameObject.SetActive(flashing); flashLight.enabled = flashing; // Pose: in front of the right shoulder, pointed at the crosshair target. Vector3 shoulder = transform.TransformPoint(shoulderOffset); Vector3 toAim = aimPoint - shoulder; Quaternion aim = toAim.sqrMagnitude > 0.5f ? Quaternion.LookRotation(toAim) : transform.rotation; float reloadTilt = 0f; if (reloading) reloadTilt = UpdateReload(); // Equip: 1 = fully lowered (muzzle down at the hip), 0 = raised. Overshoots slightly on the way up. if (equipping) { equipProgress = Mathf.Clamp01((Time.time - equipStart) / equipDuration); if (equipProgress >= 1f) equipping = false; } float lowered = 1f - EaseOutBack(equipProgress); Quaternion rotation = aim * Quaternion.Euler(-kickPitch + reloadTilt * 25f + lowered * 55f, lowered * -15f, reloadTilt * -40f + lowered * -20f); Vector3 position = shoulder + aim * (holdOffset + Vector3.back * (kickBack + lowered * 0.08f) + Vector3.down * (reloadTilt * 0.08f + lowered * 0.25f)); gun.SetPositionAndRotation(position, rotation); } // Returns 0..1 "how far the gun is tilted for the reload" and drives the magazine. float UpdateReload() { float t = (Time.time - reloadStart) / reloadDuration; if (t >= 1f) { reloading = false; magazine.localPosition = magazineRestPos; magazine.gameObject.SetActive(true); return 0f; } // Tilt in over the first 15%, hold, tilt back out over the last 15%. float tilt = Mathf.SmoothStep(0f, 1f, Mathf.Min(t / 0.15f, (1f - t) / 0.15f, 1f)); if (t >= 0.25f && !magDropped) { magDropped = true; DropMagazineCopy(); magazine.gameObject.SetActive(false); } if (t >= 0.55f) { // New mag slides up into the well. magazine.gameObject.SetActive(true); float insert = Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(0.55f, 0.8f, t)); magazine.localPosition = magazineRestPos + Vector3.down * (0.25f * (1f - insert)); } return tilt; } void DropMagazineCopy() { var dropped = Instantiate(magazine.gameObject, magazine.position, magazine.rotation); dropped.name = "DroppedMagazine"; dropped.transform.localScale = magazine.lossyScale; dropped.AddComponent(); var body = dropped.AddComponent(); body.linearVelocity = -gun.up * 1.5f; body.angularVelocity = Random.insideUnitSphere * 5f; Destroy(dropped, 4f); } void OnAnimatorIK(int layerIndex) { if (gun == null) return; animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1f); animator.SetIKPosition(AvatarIKGoal.RightHand, rightGrip.position); // During the reload the left hand goes to the magazine well instead of the handguard. Vector3 leftTarget = reloading ? magazine.position : leftGrip.position; animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1f); animator.SetIKPosition(AvatarIKGoal.LeftHand, leftTarget); animator.SetLookAtWeight(1f, 0.35f, 0.8f, 0f, 0.6f); animator.SetLookAtPosition(aimPoint); } }