From d76ed9cc5a23e558490f5e64c8ad0b502a4ec5c3 Mon Sep 17 00:00:00 2001 From: Joe Anderson Date: Sat, 28 Dec 2024 10:04:40 +0000 Subject: [PATCH] Implement charger gremlin (#93) --- .../Prefabs/Enemies/Charger Gremlin.prefab | 211 ++++++++++++++++++ .../Enemies/Charger Gremlin.prefab.meta | 7 + Assets/Unity/Scenes/Level 3.unity | 69 ++++++ Assets/src/AEnemyAI2.cs | 12 + Assets/src/ChargerGremlinAttackAI.cs | 141 ++++++++++++ Assets/src/ChargerGremlinAttackAI.cs.meta | 11 + Assets/src/CollisionMask.cs | 9 + Assets/src/EnemyHitPointEvents.cs | 7 +- Assets/src/ScheduleAttackAI.cs | 7 +- Assets/src/StayCloseToPlayerAI.cs | 4 +- 10 files changed, 471 insertions(+), 7 deletions(-) create mode 100644 Assets/Unity/Prefabs/Enemies/Charger Gremlin.prefab create mode 100644 Assets/Unity/Prefabs/Enemies/Charger Gremlin.prefab.meta create mode 100644 Assets/src/ChargerGremlinAttackAI.cs create mode 100644 Assets/src/ChargerGremlinAttackAI.cs.meta diff --git a/Assets/Unity/Prefabs/Enemies/Charger Gremlin.prefab b/Assets/Unity/Prefabs/Enemies/Charger Gremlin.prefab new file mode 100644 index 00000000..5b789ca1 --- /dev/null +++ b/Assets/Unity/Prefabs/Enemies/Charger Gremlin.prefab @@ -0,0 +1,211 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &7395253680585668139 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2090889806084682781} + - component: {fileID: 8691424312494069922} + m_Layer: 8 + m_Name: Charger Gremlin Attack AI + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2090889806084682781 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7395253680585668139} + serializedVersion: 2 + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 7674314308645791106} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &8691424312494069922 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7395253680585668139} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e994ffc91b3c2424d8b1f553b6d50799, type: 3} + m_Name: + m_EditorClassIdentifier: + IsActive: 0 + k__BackingField: 20 + PrepareSpeed: 2 + RecoverSpeed: 5 + WiggleAmplitude: 10 + PrepareWiggleSpeed: 30 + ChargeWiggleSpeed: 30 + RecoverWiggleSpeed: 10 + PrepareDuration: 1.5 + MaxChargeDuration: 2 + RecoverDuration: 4 + Damage: 20 +--- !u!1001 &8161433874373683611 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 1871121786428654615, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: Maximum + value: 60 + objectReference: {fileID: 0} + - target: {fileID: 4399600734961214489, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4399600734961214489, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4399600734961214489, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4399600734961214489, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4399600734961214489, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4399600734961214489, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4399600734961214489, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4399600734961214489, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4399600734961214489, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4399600734961214489, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6683697602069921621, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: CircleSpeed + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6683697602069921621, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: TooFarDistance + value: 9 + objectReference: {fileID: 0} + - target: {fileID: 6683697602069921621, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: TooCloseDistance + value: 3.375 + objectReference: {fileID: 0} + - target: {fileID: 6683697602069921621, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: BackOffToDistance + value: 4.5 + objectReference: {fileID: 0} + - target: {fileID: 6683697602069921621, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: ApproachToDistance + value: 5.625 + objectReference: {fileID: 0} + - target: {fileID: 7238508906334667609, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: m_Name + value: Charger Gremlin AI + objectReference: {fileID: 0} + - target: {fileID: 7343680986649598625, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: AttackAI + value: + objectReference: {fileID: 8691424312494069922} + - target: {fileID: 7343680986649598625, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: AttackInterval + value: 4 + objectReference: {fileID: 0} + - target: {fileID: 7343680986649598625, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: RandomnessFactor + value: 0.25 + objectReference: {fileID: 0} + - target: {fileID: 7343680986649598625, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: MinDelayBeforeAttack + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7343680986649598625, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: InterruptWhenOutOfRange + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7764934233349782288, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: m_Color.b + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7764934233349782288, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: m_Color.g + value: 0.65377206 + objectReference: {fileID: 0} + - target: {fileID: 7764934233349782288, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: m_Color.r + value: 0.6367924 + objectReference: {fileID: 0} + - target: {fileID: 8188467070782874524, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + propertyPath: m_Name + value: Charger Gremlin + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: + - targetCorrespondingSourceObject: {fileID: 2000610790513937433, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + insertIndex: -1 + addedObject: {fileID: 2090889806084682781} + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 104942a13dd3f4497992f707511fee7d, type: 3} +--- !u!4 &7674314308645791106 stripped +Transform: + m_CorrespondingSourceObject: {fileID: 2000610790513937433, guid: 104942a13dd3f4497992f707511fee7d, + type: 3} + m_PrefabInstance: {fileID: 8161433874373683611} + m_PrefabAsset: {fileID: 0} diff --git a/Assets/Unity/Prefabs/Enemies/Charger Gremlin.prefab.meta b/Assets/Unity/Prefabs/Enemies/Charger Gremlin.prefab.meta new file mode 100644 index 00000000..68ca8285 --- /dev/null +++ b/Assets/Unity/Prefabs/Enemies/Charger Gremlin.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4689c689dfa584f528d46af9cac12117 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Unity/Scenes/Level 3.unity b/Assets/Unity/Scenes/Level 3.unity index 9b48c78a..de706aaf 100644 --- a/Assets/Unity/Scenes/Level 3.unity +++ b/Assets/Unity/Scenes/Level 3.unity @@ -1344,6 +1344,74 @@ Transform: - {fileID: 725285167} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1001 &1090866498 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 63186410946759175, guid: 4689c689dfa584f528d46af9cac12117, + type: 3} + propertyPath: m_Name + value: Charger Gremlin + objectReference: {fileID: 0} + - target: {fileID: 5498254477281136002, guid: 4689c689dfa584f528d46af9cac12117, + type: 3} + propertyPath: m_LocalPosition.x + value: -14 + objectReference: {fileID: 0} + - target: {fileID: 5498254477281136002, guid: 4689c689dfa584f528d46af9cac12117, + type: 3} + propertyPath: m_LocalPosition.y + value: 2.8682303 + objectReference: {fileID: 0} + - target: {fileID: 5498254477281136002, guid: 4689c689dfa584f528d46af9cac12117, + type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5498254477281136002, guid: 4689c689dfa584f528d46af9cac12117, + type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 5498254477281136002, guid: 4689c689dfa584f528d46af9cac12117, + type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5498254477281136002, guid: 4689c689dfa584f528d46af9cac12117, + type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5498254477281136002, guid: 4689c689dfa584f528d46af9cac12117, + type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5498254477281136002, guid: 4689c689dfa584f528d46af9cac12117, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5498254477281136002, guid: 4689c689dfa584f528d46af9cac12117, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5498254477281136002, guid: 4689c689dfa584f528d46af9cac12117, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 4689c689dfa584f528d46af9cac12117, type: 3} --- !u!1001 &1123202356 PrefabInstance: m_ObjectHideFlags: 0 @@ -2989,3 +3057,4 @@ SceneRoots: - {fileID: 1535711301} - {fileID: 521433781861264990} - {fileID: 50370136} + - {fileID: 1090866498} diff --git a/Assets/src/AEnemyAI2.cs b/Assets/src/AEnemyAI2.cs index 05317ee8..9bb67dc8 100644 --- a/Assets/src/AEnemyAI2.cs +++ b/Assets/src/AEnemyAI2.cs @@ -11,6 +11,8 @@ protected virtual void OnActivate() { } protected virtual void OnDeactivate() { } protected virtual void UseChildAIs(Action useChild) { } + protected virtual bool ShouldAvoidInterruption() => false; + // Used only by AMovementEnemyAI protected virtual AMovementEnemyAI InternalUseMovementAI() { return null; @@ -97,6 +99,16 @@ private void DeactivateChildAI(AEnemyAI2 child) { child.Deactivate(); } + protected bool AnyDescendant(Func condition) { + if (condition(this)) + return true; + + return ActiveChildAIs.Any(child => child.AnyDescendant(condition)); + } + + protected bool AvoidingInterruption + => AnyDescendant(ai => ai.ShouldAvoidInterruption()); + private EnemyAIHelper MakeHelper() => new EnemyAIHelper( ai: this, gameObject: GetRootTransform().gameObject, diff --git a/Assets/src/ChargerGremlinAttackAI.cs b/Assets/src/ChargerGremlinAttackAI.cs new file mode 100644 index 00000000..848e920c --- /dev/null +++ b/Assets/src/ChargerGremlinAttackAI.cs @@ -0,0 +1,141 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public class ChargerGremlinAttackAI : AMovementEnemyAI { + [field: SerializeField] + public override float Speed { get; set; } + + public float PrepareSpeed, RecoverSpeed; + public float WiggleAmplitude; + public float PrepareWiggleSpeed, ChargeWiggleSpeed, RecoverWiggleSpeed; + public float PrepareDuration, MaxChargeDuration, RecoverDuration; + public float Damage; + + private enum StateType { Preparing, Charging, Recovering }; + private StateType State = StateType.Preparing; + private Transform WiggleTransform; + private EnemyHitPointEvents HitPointEvents; + private bool PreviousSlowOnDamage; + private float WigglePhase; + private Vector2 Direction; + private Stopwatch Stopwatch; + + void Start() { + Helper.OnAnyCollision((collider) => { + if (State != StateType.Charging) + return; + + if (IsBullet(collider.gameObject.layer)) + return; + + HitPoints hitPoints = collider.gameObject.GetComponent(); + + if (hitPoints) { + bool isCounterAttack = hitPoints.Damage(Damage, true); + + if (isCounterAttack) { + Helper.KillSelf(); + } + } + + SetState(StateType.Recovering); + + Helper.SetTimeout(OnFinish, RecoverDuration); + }); + + WiggleTransform = Helper.Transform.Find("Sprite"); + + HitPointEvents = Helper.Transform + .Find("Hit Point Events") + .GetComponent(); + } + + protected override void OnActivate() { + SetState(StateType.Preparing); + + Helper.SetTimeout(() => { + SetState(StateType.Charging); + DisableSlowOnDamage(); + }, PrepareDuration); + + if (MaxChargeDuration != Mathf.Infinity) { + Helper.SetTimeout(() => { + if (State == StateType.Charging) { + OnFinish(); + } + }, PrepareDuration + MaxChargeDuration); + } + + WigglePhase = 0f; + } + + protected override void OnDeactivate() { + SetWiggleAngle(0f); + ResetSlowOnDamage(); + } + + protected override bool ShouldAvoidInterruption() + => State != StateType.Preparing; + + protected override void WhileActive() { + switch (State) { + case StateType.Preparing: + WhilePreparing(); + break; + + case StateType.Charging: + WhileCharging(); + break; + + case StateType.Recovering: + WhileRecovering(); + break; + } + } + + private void WhilePreparing() { + Direction = Helper.DirectionToPlayer; + float progress = Stopwatch.Progress(PrepareDuration); + Helper.MoveWithVelocity(-1f * (1f - progress) * PrepareSpeed * Direction); + Wiggle(PrepareWiggleSpeed * progress); + } + + private void WhileCharging() { + Helper.MoveWithVelocity(Speed * Direction); + Wiggle(ChargeWiggleSpeed); + } + + private void WhileRecovering() { + float progress = 1f - Stopwatch.Progress(RecoverDuration); + float recoilProgress = Mathf.Pow(progress, 10f); + Helper.MoveWithVelocity(-1f * recoilProgress * RecoverSpeed * Direction); + Wiggle(RecoverWiggleSpeed * progress); + } + + private void SetState(StateType state) { + Stopwatch = new Stopwatch.PlayingTime(); + State = state; + } + + private void Wiggle(float speed) { + WigglePhase += speed * Time.deltaTime; + SetWiggleAngle(Mathf.Sin(WigglePhase) * WiggleAmplitude); + } + + private void SetWiggleAngle(float angle) { + WiggleTransform.rotation = Quaternion.Euler(0, 0, angle); + } + + private bool IsBullet(int layer) + => ((2 << layer) & CollisionMask.BulletMask) != 0; + + private void DisableSlowOnDamage() { + PreviousSlowOnDamage = HitPointEvents.SlowOnDamage; + HitPointEvents.SlowOnDamage = false; + } + + private void ResetSlowOnDamage() { + HitPointEvents.SlowOnDamage = PreviousSlowOnDamage; + } +} diff --git a/Assets/src/ChargerGremlinAttackAI.cs.meta b/Assets/src/ChargerGremlinAttackAI.cs.meta new file mode 100644 index 00000000..2df89fd5 --- /dev/null +++ b/Assets/src/ChargerGremlinAttackAI.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e994ffc91b3c2424d8b1f553b6d50799 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/src/CollisionMask.cs b/Assets/src/CollisionMask.cs index 226264e9..15af42b8 100644 --- a/Assets/src/CollisionMask.cs +++ b/Assets/src/CollisionMask.cs @@ -52,4 +52,13 @@ private static LayerMask GetPlayerMask() => LayerMask.GetMask("Player") | LayerMask.GetMask("PlayerRolling") | LayerMask.GetMask("PlayerCrouching"); + + private static LayerMask? _BulletMask; + public static LayerMask BulletMask => ( + _BulletMask ?? (_BulletMask = GetBulletMask()) + ).Value; + + private static LayerMask GetBulletMask() => + LayerMask.GetMask("PlayerBullet") | + LayerMask.GetMask("EnemyBullet"); } diff --git a/Assets/src/EnemyHitPointEvents.cs b/Assets/src/EnemyHitPointEvents.cs index 5d49322d..b1584fcd 100644 --- a/Assets/src/EnemyHitPointEvents.cs +++ b/Assets/src/EnemyHitPointEvents.cs @@ -14,7 +14,8 @@ public class EnemyHitPointEvents : MonoBehaviour { public HUDBar HealthBar; public bool HideHealthBarWhenFullOrEmpty; - public float SlowOnDamageDuration = 0f; + public bool SlowOnDamage = true; + public float SlowOnDamageDuration; public MovementManager MovementManager; private Stopwatch SlowStopwatch = null; @@ -36,7 +37,7 @@ void Awake() { HitPoints.OnDecrease.AddListener(hp => { Flash?.BeginFlashing(); - if (SlowOnDamageDuration > 0f) { + if (SlowOnDamage) { SlowStopwatch = new Stopwatch.PlayingTime(); } }); @@ -48,7 +49,7 @@ void Awake() { }); MovementManager?.SpeedModifiers.Add((speed) => { - if (SlowStopwatch == null) + if (!SlowOnDamage || SlowStopwatch == null) return speed; float progress = SlowStopwatch.Progress(SlowOnDamageDuration); diff --git a/Assets/src/ScheduleAttackAI.cs b/Assets/src/ScheduleAttackAI.cs index 2a065fc8..9b381d62 100644 --- a/Assets/src/ScheduleAttackAI.cs +++ b/Assets/src/ScheduleAttackAI.cs @@ -20,6 +20,9 @@ public class ScheduleAttackAI : AMovementEnemyAI { void Start() { if (InterruptWhenDamaged) { Helper.OnDamage(() => { + if (AvoidingInterruption) + return; + if (Attacking) { StopAttack(); } else { @@ -67,8 +70,6 @@ protected override AMovementEnemyAI UseMovementAI() => Attacking private bool WithinRange => Helper.DistanceToPlayer <= MaxDistance; - public AMovementEnemyAI IfDamaged, IfNotDamaged; - private void OnEnterRange() { if (!Attacking && AttackTimer == null) { ScheduleAttack(); @@ -80,7 +81,7 @@ private void OnExitRange() { UnscheduleAttack(); } - if (Attacking && InterruptWhenOutOfRange) { + if (Attacking && InterruptWhenOutOfRange && !AvoidingInterruption) { StopAttack(); } } diff --git a/Assets/src/StayCloseToPlayerAI.cs b/Assets/src/StayCloseToPlayerAI.cs index fdec93fd..58d64a43 100644 --- a/Assets/src/StayCloseToPlayerAI.cs +++ b/Assets/src/StayCloseToPlayerAI.cs @@ -22,7 +22,9 @@ protected override void OnActivate() { protected override void WhileActive() { LowPriorityBehaviour.EveryNFrames(10, () => { - CanMoveToPlayer = Helper.CanMoveToPlayer(); + if (!AvoidingInterruption) { + CanMoveToPlayer = Helper.CanMoveToPlayer(); + } }); }