diff --git a/Assets/Prefabs/Tower.prefab b/Assets/Prefabs/Tower.prefab index cc673093afaff645b43be476985f1302238e8cd1..01597522a044f8aad9eeb2d34a8cb95d5c4ab317 100644 --- a/Assets/Prefabs/Tower.prefab +++ b/Assets/Prefabs/Tower.prefab @@ -11,6 +11,7 @@ GameObject: - component: {fileID: 2849581468949299191} - component: {fileID: 7040201590077353716} - component: {fileID: -5257472747468331865} + - component: {fileID: 4195717166071304880} m_Layer: 0 m_Name: Tower m_TagString: Untagged @@ -95,5 +96,101 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 67aafe08abf8467fb869765ad1410d15, type: 3} m_Name: m_EditorClassIdentifier: - blueTowerData: {fileID: 11400000, guid: 7a55e55983e119942a28e07adc7bb9e3, type: 2} - redTowerData: {fileID: 11400000, guid: 1a87325dc264be941b2d558ea6e8987b, type: 2} +--- !u!120 &4195717166071304880 +LineRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1091641560809420229} + m_Enabled: 0 + m_CastShadows: 0 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 0 + m_LightProbeUsage: 0 + m_ReflectionProbeUsage: 0 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 10306, guid: 0000000000000000f000000000000000, type: 0} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: -1196961719 + m_SortingLayer: 2 + m_SortingOrder: 0 + m_Positions: + - {x: 0, y: 0, z: 0} + - {x: 0, y: 0, z: 0} + m_Parameters: + serializedVersion: 3 + widthMultiplier: 0.2037388 + widthCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + colorGradient: + serializedVersion: 2 + key0: {r: 0, g: 1, b: 0, a: 1} + key1: {r: 0, g: 1, b: 0, a: 1} + key2: {r: 0, g: 0, b: 0, a: 0} + key3: {r: 0, g: 0, b: 0, a: 0} + key4: {r: 0, g: 0, b: 0, a: 0} + key5: {r: 0, g: 0, b: 0, a: 0} + key6: {r: 0, g: 0, b: 0, a: 0} + key7: {r: 0, g: 0, b: 0, a: 0} + ctime0: 0 + ctime1: 65535 + ctime2: 0 + ctime3: 0 + ctime4: 0 + ctime5: 0 + ctime6: 0 + ctime7: 0 + atime0: 0 + atime1: 65535 + atime2: 0 + atime3: 0 + atime4: 0 + atime5: 0 + atime6: 0 + atime7: 0 + m_Mode: 1 + m_NumColorKeys: 2 + m_NumAlphaKeys: 2 + numCornerVertices: 0 + numCapVertices: 10 + alignment: 0 + textureMode: 0 + shadowBias: 0.5 + generateLightingData: 0 + m_UseWorldSpace: 1 + m_Loop: 0 diff --git a/Assets/Scripts/Logic/Event/EventDispatcher.cs b/Assets/Scripts/Logic/Event/EventDispatcher.cs index b2843bc7b8a65bc4d375a5d68a1be601e60f439b..bccf6bbb536fba293a38c67a47bfdb60755bfb55 100644 --- a/Assets/Scripts/Logic/Event/EventDispatcher.cs +++ b/Assets/Scripts/Logic/Event/EventDispatcher.cs @@ -1,5 +1,6 @@ ďťżusing System; using System.Collections.Generic; +using System.Linq; namespace Logic.Event { @@ -16,7 +17,9 @@ namespace Logic.Event { /// </p> /// </summary> public class EventDispatcher { - private readonly IDictionary<Type, IList<Delegate>> _listeners = new Dictionary<Type, IList<Delegate>>(); + private readonly IDictionary<Type, IList<RegisteredListener>> _listeners = + new Dictionary<Type, IList<RegisteredListener>>(); + private readonly Action<ICollection<EventInvocationException>> _eventRaisingErrorHandler; /// <summary> @@ -28,6 +31,13 @@ public class EventDispatcher { _eventRaisingErrorHandler = eventRaisingErrorHandler; } + /// <summary> + /// Same as calling <see cref="AddListener{T}(Ordering, Listener{T})"/> with <see cref="Ordering.Normal"/>. + /// </summary> + public void AddListener<T>(Listener<T> listener) where T : BaseEvent { + AddListener(Ordering.Normal, listener); + } + /// <summary> /// Registers a new listener to the specified event. /// The same listener is allowed to listen to the same event multiple times. @@ -35,15 +45,16 @@ public class EventDispatcher { /// A listener is also called if a superclass of the specified event is raised, /// see <see cref="Raise"/> for more information about this. /// </summary> + /// <param name="ordering">when the listener should be called relative to other listeners</param> /// <param name="listener">the consumer of the event</param> /// <typeparam name="T">the event to listen to</typeparam> - public void AddListener<T>(Listener<T> listener) where T : BaseEvent { - if (!_listeners.TryGetValue(typeof(T), out IList<Delegate> list)) { - list = new List<Delegate>(); - _listeners[typeof(T)] = list; + public void AddListener<T>(Ordering ordering, Listener<T> listener) where T : BaseEvent { + if (!_listeners.TryGetValue(typeof(T), out IList<RegisteredListener> set)) { + set = new List<RegisteredListener>(); + _listeners[typeof(T)] = set; } - list.Add(listener); + set.Add(new RegisteredListener(ordering, listener)); } /// <summary> @@ -52,13 +63,15 @@ public class EventDispatcher { /// If the same listener was registered multiple times, /// then this method only removes one occurrence of the listener. /// </summary> + /// <param name="ordering">the ordering of the listener that should be unregistered</param> /// <param name="listener">the listener to unregister</param> /// <typeparam name="T">the event to unregister the listener from</typeparam> /// <exception cref="IllegalListenerStateException">if the specified listener isn't /// among the listeners of the specified event</exception> - public void RemoveListener<T>(Listener<T> listener) where T : BaseEvent { - if (!_listeners.TryGetValue(typeof(T), out IList<Delegate> list) || !list.Remove(listener)) { - throw new IllegalListenerStateException($"{listener} isn't a registered listener of {typeof(T)}"); + public void RemoveListener<T>(Ordering ordering, Listener<T> listener) where T : BaseEvent { + RegisteredListener registered = new RegisteredListener(ordering, listener); + if (!_listeners.TryGetValue(typeof(T), out IList<RegisteredListener> set) || !set.Remove(registered)) { + throw new IllegalListenerStateException($"{registered} isn't a registered listener of {typeof(T)}"); } } @@ -71,28 +84,43 @@ public class EventDispatcher { public void Raise(BaseEvent eventData) { List<EventInvocationException> errors = new List<EventInvocationException>(); - //Call listeners which listen to the exact event type or one of its superclasses - Type type = eventData.GetType(); - do { - // ReSharper disable once AssignNullToNotNullAttribute - if (!_listeners.TryGetValue(type, out IList<Delegate> listeners)) break; - - foreach (Delegate del in listeners) { - try { - del.DynamicInvoke(eventData); - } catch (Exception e) { - errors.Add(new EventInvocationException($"Error consuming {eventData} by {del}", e)); + foreach (Ordering ordering in Enum.GetValues(typeof(Ordering))) { + //Call listeners which listen to the exact event type or one of its superclasses + Type type = eventData.GetType(); + do { + // ReSharper disable once AssignNullToNotNullAttribute + if (!_listeners.TryGetValue(type, out IList<RegisteredListener> listeners)) break; + + foreach (RegisteredListener registered in listeners.Where(x => x.Order == ordering)) { + try { + registered.Listener.DynamicInvoke(eventData); + } catch (Exception e) { + errors.Add(new EventInvocationException($"Error consuming {eventData} by {registered}", e)); + } } - } - type = type.BaseType; - } while (type != typeof(object)); + type = type.BaseType; + } while (type != typeof(object)); + } if (errors.Count > 0) { _eventRaisingErrorHandler.Invoke(errors); } } + /// <summary> + /// Determines when a <see cref="Listener{T}"/> should be called + /// relative to other listeners. + /// The order when multiple listeners share the same enum value is unspecified. + /// </summary> + public enum Ordering { + First, + Sooner, + Normal, + Later, + Last + } + /// <summary> /// Consumes an event and executes action(s) based on said event. /// </summary> @@ -112,6 +140,20 @@ public class EventDispatcher { public class EventInvocationException : Exception { public EventInvocationException(string message, Exception e) : base(message, e) {} } + + private readonly struct RegisteredListener { + public Ordering Order { get; } + public Delegate Listener { get; } + + public RegisteredListener(Ordering order, Delegate listener) { + Order = order; + Listener = listener; + } + + public override string ToString() { + return "{" + Order + ">>" + Listener + "}"; + } + } } } diff --git a/Assets/Scripts/Logic/System/DestroyUnitSystem.cs b/Assets/Scripts/Logic/System/DestroyUnitSystem.cs index 59ecf1aa5d4a1308e7f8cf57b31bad33edf28685..ea77f25b5d9fce6392063ae30026f1354e75c16c 100644 --- a/Assets/Scripts/Logic/System/DestroyUnitSystem.cs +++ b/Assets/Scripts/Logic/System/DestroyUnitSystem.cs @@ -5,7 +5,9 @@ using Logic.Event.World.Unit; namespace Logic.System { public class DestroyUnitSystem : BaseSystem { public override void RegisterListeners(EventDispatcher dispatcher) { - dispatcher.AddListener<UnitDamagedEvent>(On); + //Call later than usual: let listeners get notified about the damaged event + // before the destroyed event is invoked. + dispatcher.AddListener<UnitDamagedEvent>(EventDispatcher.Ordering.Later, On); } private void On(UnitDamagedEvent e) { diff --git a/Assets/Scripts/LogicTests/EventDispatcherTest.cs b/Assets/Scripts/LogicTests/EventDispatcherTest.cs index 64e61f8d146a77cd63d3113cd1a76eacc3fdee9c..2b9cea50cdd9d6f98f3581454da1da675941418c 100644 --- a/Assets/Scripts/LogicTests/EventDispatcherTest.cs +++ b/Assets/Scripts/LogicTests/EventDispatcherTest.cs @@ -19,13 +19,17 @@ public class EventDispatcherTest { EventDispatcher dispatcher = NewErrorRethrowingDispatcher(); EventDispatcher.Listener<BaseEvent> listener = _ => {}; Assert.Throws<EventDispatcher.IllegalListenerStateException>(() => - dispatcher.RemoveListener<BicycleEvent>(listener)); + dispatcher.RemoveListener<BicycleEvent>(EventDispatcher.Ordering.Normal, listener)); dispatcher.AddListener<BicycleEvent>(listener); dispatcher.AddListener<BicycleEvent>(listener); - dispatcher.RemoveListener<BicycleEvent>(listener); - dispatcher.RemoveListener<BicycleEvent>(listener); + dispatcher.AddListener<BicycleEvent>(EventDispatcher.Ordering.First, listener); + dispatcher.RemoveListener<BicycleEvent>(EventDispatcher.Ordering.Normal, listener); + dispatcher.RemoveListener<BicycleEvent>(EventDispatcher.Ordering.Normal, listener); Assert.Throws<EventDispatcher.IllegalListenerStateException>(() => - dispatcher.RemoveListener<BicycleEvent>(listener)); + dispatcher.RemoveListener<BicycleEvent>(EventDispatcher.Ordering.Normal, listener)); + dispatcher.RemoveListener<BicycleEvent>(EventDispatcher.Ordering.First, listener); + Assert.Throws<EventDispatcher.IllegalListenerStateException>(() => + dispatcher.RemoveListener<BicycleEvent>(EventDispatcher.Ordering.First, listener)); } [Test] @@ -116,6 +120,21 @@ public class EventDispatcherTest { Assert.IsTrue(called); } + [Test] + public void TestInvocationOrder() { + EventDispatcher dispatcher = NewErrorRethrowingDispatcher(); + var concat = "-"; + // ReSharper disable once AccessToModifiedClosure + dispatcher.AddListener<BicycleEvent>(_ => concat += "3"); + dispatcher.AddListener<BaseEvent>(EventDispatcher.Ordering.Normal, _ => concat += "3"); + dispatcher.AddListener<BaseEvent>(EventDispatcher.Ordering.Sooner, _ => concat += "2"); + dispatcher.AddListener<BicycleEvent>(EventDispatcher.Ordering.Last, _ => concat += "5"); + dispatcher.AddListener<BicycleEvent>(EventDispatcher.Ordering.First, _ => concat += "1"); + dispatcher.AddListener<BicycleEvent>(EventDispatcher.Ordering.Later, _ => concat += "4"); + dispatcher.Raise(new BicycleEvent()); + Assert.AreEqual("-123345", concat); + } + private static EventDispatcher NewErrorRethrowingDispatcher() { return new EventDispatcher(errors => throw new AggregateException(errors)); } diff --git a/Assets/Scripts/Presentation/World/Barrack.cs b/Assets/Scripts/Presentation/World/Barrack.cs index 950505fa222479ec71fb22232e43e26532c64942..c4d89e64c88ba4502a88bf2658062b57f10de504 100644 --- a/Assets/Scripts/Presentation/World/Barrack.cs +++ b/Assets/Scripts/Presentation/World/Barrack.cs @@ -3,7 +3,7 @@ using Color = Logic.Data.Color; namespace Presentation.World { [RequireComponent(typeof(SpriteRenderer))] -public class Barrack : MonoBehaviour { +public class Barrack : Structure { [SerializeField] private BarrackData blueBarrackData; diff --git a/Assets/Scripts/Presentation/World/Tower.cs b/Assets/Scripts/Presentation/World/Tower.cs index e3ccd6952ff1c58be2ac618440d56555fb80a4f0..8de3600e8ac183bad90688ea58a2cf9a9db1daef 100644 --- a/Assets/Scripts/Presentation/World/Tower.cs +++ b/Assets/Scripts/Presentation/World/Tower.cs @@ -3,6 +3,7 @@ using Color = Logic.Data.Color; namespace Presentation.World { [RequireComponent(typeof(SpriteRenderer))] +[RequireComponent(typeof(LineRenderer))] public class Tower : Structure { private Logic.Data.World.Tower _data; @@ -15,6 +16,12 @@ public class Tower : Structure { _spriteRenderer = GetComponent<SpriteRenderer>(); _spriteRenderer.sprite = data.Sprite; _spriteRenderer.color = color; + + LineRenderer laserRenderer = GetComponent<LineRenderer>(); + Gradient laserGradient = new Gradient(); + laserGradient.SetKeys(new[] { new GradientColorKey(color, 0), new GradientColorKey(color, 1) }, + new[] { new GradientAlphaKey(1, 0), new GradientAlphaKey(1, 1) }); + laserRenderer.colorGradient = laserGradient; } public void SetData(Logic.Data.World.Tower data) { diff --git a/Assets/Scripts/Presentation/World/World.cs b/Assets/Scripts/Presentation/World/World.cs index 0d21a8e94a0a606fe1b33576bb7ff4a06b87525a..d803aac18a434f9f81b093e1204bf0ac4bfc69e9 100644 --- a/Assets/Scripts/Presentation/World/World.cs +++ b/Assets/Scripts/Presentation/World/World.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using Logic.Data.World; using Logic.Event.World.Tower; @@ -14,17 +15,22 @@ public class World : MonoBehaviour { public GameObject Castle; public GameObject Unit; - public float TilePadding = 0.1f; - private GameObject[,] _map; private Dictionary<Logic.Data.World.Unit, Unit> _units; + public T LogicToPresentation<T>(TileObject tileObject) where T : Structure + => _map[tileObject.Position.X, tileObject.Position.Y].GetComponentInChildren<T>(); + + public Unit LogicToPresentation(Logic.Data.World.Unit unit) => _units[unit]; + private void Start() { var simulation = FindObjectOfType<SimulationManager>(); _units = new Dictionary<Logic.Data.World.Unit, Unit>(); simulation.GameOverview.Events.AddListener<TowerBuiltEvent>(OnTowerBuilt); + simulation.GameOverview.Events.AddListener<TowerShotEvent>(OnTowerShot); + simulation.GameOverview.Events.AddListener<TowerCooledDownEvent>(OnTowerCooledDown); simulation.GameOverview.Events.AddListener<UnitDeployedEvent>(OnUnitDeployed); simulation.GameOverview.Events.AddListener<UnitMovedTileEvent>(OnUnitMovedTile); @@ -76,6 +82,40 @@ public class World : MonoBehaviour { InstantiateTower(_map[tilePosition.X, tilePosition.Y], tower); } + private void OnTowerShot(TowerShotEvent e) { + Tower tower = LogicToPresentation<Tower>(e.Tower); + LineRenderer laserRenderer = tower.GetComponent<LineRenderer>(); + Transform target = LogicToPresentation(e.Target).transform; + + Vector3[] positions = { tower.transform.position, target.position }; + laserRenderer.SetPositions(positions); + laserRenderer.enabled = true; + + IEnumerator LaserUpdater() { + var remainingTime = 0.15f; + while (remainingTime > 0) { + yield return new WaitForFixedUpdate(); + remainingTime -= Time.fixedDeltaTime; + + if (e.Target.IsAlive) { + positions[1] = target.position; + laserRenderer.SetPositions(positions); + } + } + + laserRenderer.enabled = false; + } + + StartCoroutine(LaserUpdater()); + } + + private void OnTowerCooledDown(TowerCooledDownEvent e) { + Tower tower = LogicToPresentation<Tower>(e.Tower); + //Disable the laser renderer when the tower is ready to shoot again: + // we want to avoid activating a new laser pulse and then deactivating the old one + tower.GetComponent<LineRenderer>().enabled = false; + } + private GameObject InstantiateTower(GameObject parent, Logic.Data.World.Tower tower) { GameObject structure = Instantiate(Tower, parent.transform); var barrackComponent = structure.GetComponent<Tower>(); @@ -84,12 +124,8 @@ public class World : MonoBehaviour { } private GameObject InstantiateTile(int x, int y, GameWorld world) { - float sizeMultiplier = TilePadding + 1.0f; - - Vector3 position = new Vector3(x, y) * sizeMultiplier; - GameObject tile = Instantiate(Tile, transform); - tile.transform.localPosition = position; + tile.transform.localPosition = new Vector3(x, y); var tileComponent = tile.GetComponent<Tile>(); tileComponent.Position = new TilePosition(x, y); _map[x, y] = tile; diff --git a/ProjectSettings/TagManager.asset b/ProjectSettings/TagManager.asset index 9e30ccc19d643102a74868b8d6b3efc899bb239f..5900abf1cfa1a9438abf63ca2fe22cd2e188bb9c 100644 --- a/ProjectSettings/TagManager.asset +++ b/ProjectSettings/TagManager.asset @@ -47,6 +47,9 @@ TagManager: - name: Structure uniqueID: 520840465 locked: 0 + - name: Projectile + uniqueID: 3098005577 + locked: 0 - name: Unit uniqueID: 1800257023 locked: 0