From b3c10c089f1cb18483d783b332069cdba7b5fcf8 Mon Sep 17 00:00:00 2001 From: Diogo Andrade Date: Fri, 31 May 2024 11:14:27 +0100 Subject: [PATCH] - Trigger will no longer throw an exception when an action is null - Removed some GC from condition evaluation - Renamed 'Box' mode on camera follow to 'Camera Trap' - Added Exponential Decay camera mode, which is like Feedback Loop, but frame independent - Added function to convert a path to world/local space, plus the ability to do that in multiple objects at the same time --- .../Scripts/Editor/CameraFollow2dEditor.cs | 7 +- .../Scripts/Editor/PathWaypointsEditor.cs | 149 +++++++++++++----- .../Scripts/Hypertag/HypertaggedObject.cs | 14 ++ .../Scripts/Systems/CameraFollow2d.cs | 47 ++++-- Assets/OkapiKit/Scripts/Systems/Path.cs | 31 +++- Assets/OkapiKit/Scripts/Triggers/Condition.cs | 18 ++- Assets/OkapiKit/Scripts/Triggers/Trigger.cs | 17 +- Assets/OkapiKit/package.json | 2 +- Assets/OkapiKitSamples/package.json | 4 +- ReleaseNotes.md | 8 + 10 files changed, 223 insertions(+), 74 deletions(-) diff --git a/Assets/OkapiKit/Scripts/Editor/CameraFollow2dEditor.cs b/Assets/OkapiKit/Scripts/Editor/CameraFollow2dEditor.cs index 8222a44e..3e04b1da 100644 --- a/Assets/OkapiKit/Scripts/Editor/CameraFollow2dEditor.cs +++ b/Assets/OkapiKit/Scripts/Editor/CameraFollow2dEditor.cs @@ -72,11 +72,14 @@ public override void OnInspectorGUI() } } - if (propMode.intValue == (int)CameraFollow2d.Mode.SimpleFeedbackLoop) + var mode = (CameraFollow2d.Mode)propMode.intValue; + + if ((mode == CameraFollow2d.Mode.SimpleFeedbackLoop) || + (mode == CameraFollow2d.Mode.ExponentialDecay)) { EditorGUILayout.PropertyField(propFollowSpeed, new GUIContent("Follow Speed", "What's the speed of the camera while following, expressed as percentage per frame.\nIf 1, camera will be locked to the target.\nUsually a value like 0.05 (5% per frame) works fine.")); } - else if (propMode.intValue == (int)CameraFollow2d.Mode.Box) + else if (mode == CameraFollow2d.Mode.CameraTrap) { EditorGUILayout.PropertyField(propRect, new GUIContent("Box", "Camera trap position/size, you can see it in magenta on the scene view.")); } diff --git a/Assets/OkapiKit/Scripts/Editor/PathWaypointsEditor.cs b/Assets/OkapiKit/Scripts/Editor/PathWaypointsEditor.cs index a906027c..0c103223 100644 --- a/Assets/OkapiKit/Scripts/Editor/PathWaypointsEditor.cs +++ b/Assets/OkapiKit/Scripts/Editor/PathWaypointsEditor.cs @@ -5,7 +5,7 @@ namespace OkapiKit.Editor { - [CustomEditor(typeof(Path))] + [CustomEditor(typeof(Path)), CanEditMultipleObjects] public class PathEditor : OkapiBaseEditor { SerializedProperty propType; @@ -48,75 +48,142 @@ public override void OnInspectorGUI() { var t = (target as Path); - EditorGUI.BeginChangeCheck(); + if (targets.Length == 1) + { + EditorGUI.BeginChangeCheck(); - var type = (Path.Type)propType.intValue; + var type = (Path.Type)propType.intValue; - EditorGUILayout.PropertyField(propType, new GUIContent("Type", "Type of path.\nLinear: Straight lines between points\nSmooth: Curved line that passes through some points and is influenced by the others.\nCircle: The first point defines the center, the second the radius of the circle. If there is a third point, it defines the radius in that approximate direction.\nArc: First point defines the center, the second and third define the beginning and end of an arc centered on the first point.\nPolygon: First point define the center, the second and third point define the radius in different directions, while the 'Sides' property defines the number of sides of the polygon.")); - if ((type != Path.Type.Circle) && (type != Path.Type.Arc)) - { - EditorGUILayout.PropertyField(propClosed, new GUIContent("Closed", "If the path should end where it starts.")); - } - if (type == Path.Type.Polygon) - { - EditorGUILayout.PropertyField(propSides, new GUIContent("Sides", "Number of sides in the polygon.")); - } - EditorGUILayout.PropertyField(propPoints, new GUIContent("Points", "Waypoints")); - EditorGUILayout.PropertyField(propWorldSpace, new GUIContent("World Space", "Are the positions in world space, or relative to this object.")); - EditorGUILayout.PropertyField(propEditMode, new GUIContent("Edit Mode", "If edit mode is on, you can edit the points the scene view.\nClick on a point to select it, use the gizmo to move them around.")); - EditorGUILayout.PropertyField(propOnlyDisplayWhenSelected, new GUIContent("Only display when selected", "If on, it will only display the path when the object is selected, otherwise it will show the object with the selected color.")); - EditorGUILayout.PropertyField(propDisplayColor, new GUIContent("Display Color", "What color should the path be rendered when not being edited")); + EditorGUILayout.PropertyField(propType, new GUIContent("Type", "Type of path.\nLinear: Straight lines between points\nSmooth: Curved line that passes through some points and is influenced by the others.\nCircle: The first point defines the center, the second the radius of the circle. If there is a third point, it defines the radius in that approximate direction.\nArc: First point defines the center, the second and third define the beginning and end of an arc centered on the first point.\nPolygon: First point define the center, the second and third point define the radius in different directions, while the 'Sides' property defines the number of sides of the polygon.")); + if ((type != Path.Type.Circle) && (type != Path.Type.Arc)) + { + EditorGUILayout.PropertyField(propClosed, new GUIContent("Closed", "If the path should end where it starts.")); + } + if (type == Path.Type.Polygon) + { + EditorGUILayout.PropertyField(propSides, new GUIContent("Sides", "Number of sides in the polygon.")); + } + EditorGUILayout.PropertyField(propPoints, new GUIContent("Points", "Waypoints")); + EditorGUILayout.PropertyField(propWorldSpace, new GUIContent("World Space", "Are the positions in world space, or relative to this object.")); + EditorGUILayout.PropertyField(propEditMode, new GUIContent("Edit Mode", "If edit mode is on, you can edit the points the scene view.\nClick on a point to select it, use the gizmo to move them around.")); + EditorGUILayout.PropertyField(propOnlyDisplayWhenSelected, new GUIContent("Only display when selected", "If on, it will only display the path when the object is selected, otherwise it will show the object with the selected color.")); + EditorGUILayout.PropertyField(propDisplayColor, new GUIContent("Display Color", "What color should the path be rendered when not being edited")); + + EditorGUILayout.PropertyField(propDescription, new GUIContent("Description", "This is for you to leave a comment for yourself or others.")); - EditorGUILayout.PropertyField(propDescription, new GUIContent("Description", "This is for you to leave a comment for yourself or others.")); + if (EditorGUI.EndChangeCheck()) + { + serializedObject.ApplyModifiedProperties(); + } - bool prevEnabled = GUI.enabled; - GUI.enabled = (propPoints.arraySize < 3) || ((type != Path.Type.Circle) && (type != Path.Type.Arc) && (type != Path.Type.Polygon)); + bool prevEnabled = GUI.enabled; + GUI.enabled = (propPoints.arraySize < 3) || ((type != Path.Type.Circle) && (type != Path.Type.Arc) && (type != Path.Type.Polygon)); - if (type == Path.Type.Smooth) - { - if (GUILayout.Button("Add Segment")) + if (type == Path.Type.Smooth) { - t.AddPoint(); - // Change to edit mode - propEditMode.boolValue = true; + if (GUILayout.Button("Add Segment")) + { + t.AddPoint(); + serializedObject.Update(); + + // Change to edit mode + propEditMode.boolValue = true; + serializedObject.ApplyModifiedProperties(); + + EditorUtility.SetDirty(target); + } + } + else + { + if (GUILayout.Button("Add Point")) + { + t.AddPoint(); + serializedObject.Update(); + + // Change to edit mode + propEditMode.boolValue = true; + serializedObject.ApplyModifiedProperties(); - EditorUtility.SetDirty(target); + EditorUtility.SetDirty(target); + } } + + GUI.enabled = prevEnabled; } - else + + bool repaint = false; + + bool canInvert = true; + foreach (Path tpath in targets) { - if (GUILayout.Button("Add Point")) + if ((tpath.pathType != Path.Type.Linear) && + (tpath.pathType != Path.Type.Smooth) && + (tpath.pathType != Path.Type.Arc)) { - t.AddPoint(); - // Change to edit mode - propEditMode.boolValue = true; - - EditorUtility.SetDirty(target); + canInvert = false; + break; } } - GUI.enabled = prevEnabled; - if ((type == Path.Type.Linear) || (type == Path.Type.Smooth) || (type == Path.Type.Arc)) + if (canInvert) { if (GUILayout.Button("Invert Path")) { Undo.RecordObject(target, "Invert path"); t.InvertPath(); + serializedObject.Update(); - SceneView.RepaintAll(); + repaint = true; } } if (GUILayout.Button("Center Path")) { Undo.RecordObject(target, "Center path"); - t.CenterPath(); + foreach (Path tpath in targets) tpath.CenterPath(); + serializedObject.Update(); + + repaint = true; + } + + bool allInSameSpace = true; + bool isWorld = (targets[0] as Path).isWorldSpace; + for (int i = 1; i < targets.Length; i++) + { + if ((targets[i] as Path).isWorldSpace != isWorld) + { + allInSameSpace = false; + break; + } + } + + if (allInSameSpace) + { + string convertButtonText = (propWorldSpace.boolValue) ? ("Convert to Local Space") : ("Convert to World Space"); - SceneView.RepaintAll(); + if (GUILayout.Button(convertButtonText)) + { + Undo.RecordObject(target, convertButtonText); + + foreach (Path tpath in targets) + { + if (propWorldSpace.boolValue) + tpath.ConvertToLocalSpace(); + else + tpath.ConvertToWorldSpace(); + } + serializedObject.Update(); + + propWorldSpace.boolValue = !propWorldSpace.boolValue; + serializedObject.ApplyModifiedProperties(); + + foreach (Path tpath in targets) EditorUtility.SetDirty(tpath); + + repaint = true; + } } - EditorGUI.EndChangeCheck(); + if (repaint) SceneView.RepaintAll(); - serializedObject.ApplyModifiedProperties(); (target as OkapiElement).UpdateExplanation(); } } diff --git a/Assets/OkapiKit/Scripts/Hypertag/HypertaggedObject.cs b/Assets/OkapiKit/Scripts/Hypertag/HypertaggedObject.cs index 07e53d9b..f10f148d 100644 --- a/Assets/OkapiKit/Scripts/Hypertag/HypertaggedObject.cs +++ b/Assets/OkapiKit/Scripts/Hypertag/HypertaggedObject.cs @@ -125,6 +125,13 @@ public static List FindGameObjectsWithHypertag(Hypertag[] tags) { List ret = new List(); + FindGameObjectsWithHypertag(tags, ret); + + return ret; + } + + public static List FindGameObjectsWithHypertag(Hypertag[] tags, List ret) + { foreach (var t in tags) { hypertaggedObjects.TryGetValue(t, out var list); @@ -141,6 +148,13 @@ public static List FindGameObjectsWithHypertag(Hypertag tag) { List ret = new List(); + FindGameObjectsWithHypertag(tag, ret); + + return ret; + } + + public static List FindGameObjectsWithHypertag(Hypertag tag, List ret) + { hypertaggedObjects.TryGetValue(tag, out var list); if (list != null) { diff --git a/Assets/OkapiKit/Scripts/Systems/CameraFollow2d.cs b/Assets/OkapiKit/Scripts/Systems/CameraFollow2d.cs index 29d791cb..0884f5ec 100644 --- a/Assets/OkapiKit/Scripts/Systems/CameraFollow2d.cs +++ b/Assets/OkapiKit/Scripts/Systems/CameraFollow2d.cs @@ -8,7 +8,7 @@ namespace OkapiKit [AddComponentMenu("Okapi/Other/Camera Follow")] public class CameraFollow2d : OkapiElement { - public enum Mode { SimpleFeedbackLoop = 0, Box = 1 }; + public enum Mode { SimpleFeedbackLoop = 0, CameraTrap = 1, ExponentialDecay = 2 }; public enum TagMode { Closest = 0, Furthest = 1, Average = 2 }; [SerializeField] Mode mode = Mode.SimpleFeedbackLoop; @@ -24,6 +24,7 @@ public enum TagMode { Closest = 0, Furthest = 1, Average = 2 }; private new Camera camera; private Bounds allObjectsBound; + private List potentialTransforms = new(); protected override string Internal_UpdateExplanation() { @@ -66,10 +67,14 @@ protected override string Internal_UpdateExplanation() { _explanation += $"It will trail the target, closing {100 * followSpeed}% of the distance each frame.\n"; } - else + else if (mode == Mode.CameraTrap) { _explanation += $"It will box the target within the given rect.\n"; } + else if (mode == Mode.ExponentialDecay) + { + _explanation += $"It will trail the target, closing {100 * followSpeed}% of the distance each second.\n"; + } if (cameraLimits) { @@ -105,7 +110,7 @@ void Start() { camera = GetComponent(); - if (mode == Mode.Box) + if (mode == Mode.CameraTrap) { float currentZ = transform.position.z; Vector3 targetPos = GetTargetPos(); @@ -122,9 +127,12 @@ void FixedUpdate() case Mode.SimpleFeedbackLoop: FixedUpdate_SimpleFeedbackLoop(); break; - case Mode.Box: + case Mode.CameraTrap: FixedUpdate_Box(); break; + case Mode.ExponentialDecay: + FixedUpdate_ExponentialDecay(); + break; } } @@ -142,6 +150,19 @@ void FixedUpdate_SimpleFeedbackLoop() RunZoom(); CheckBounds(); } + void FixedUpdate_ExponentialDecay() + { + // Nice explanation of this: https://www.youtube.com/watch?v=LSNQuFEDOyQ&ab_channel=FreyaHolm%C3%A9r + Vector3 targetPos = GetTargetPos(); + + Vector3 newPos = targetPos + (transform.position - targetPos) * Mathf.Pow((1.0f - followSpeed), Time.fixedDeltaTime); + newPos.z = transform.position.z; + + transform.position = newPos; + + RunZoom(); + CheckBounds(); + } void FixedUpdate_Box() { @@ -204,11 +225,13 @@ public Vector3 GetTargetPos() else if (targetTag) { Vector3 selectedPosition = transform.position; - var potentialObjects = gameObject.FindObjectsOfTypeWithHypertag(targetTag); + + potentialTransforms.Clear(); + gameObject.FindObjectsOfTypeWithHypertag(targetTag, potentialTransforms); if (tagMode == TagMode.Closest) { var minDist = float.MaxValue; - foreach (var obj in potentialObjects) + foreach (var obj in potentialTransforms) { var d = Vector3.Distance(obj.position, transform.position); if (d < minDist) @@ -221,7 +244,7 @@ public Vector3 GetTargetPos() else if (tagMode == TagMode.Furthest) { var maxDist = 0.0f; - foreach (var obj in potentialObjects) + foreach (var obj in potentialTransforms) { var d = Vector3.Distance(obj.position, transform.position); if (d > maxDist) @@ -233,17 +256,17 @@ public Vector3 GetTargetPos() } else if (tagMode == TagMode.Average) { - if (potentialObjects.Length > 0) + if (potentialTransforms.Count > 0) { - allObjectsBound = new Bounds(potentialObjects[0].position, Vector3.zero); + allObjectsBound = new Bounds(potentialTransforms[0].position, Vector3.zero); selectedPosition = Vector3.zero; - foreach (var obj in potentialObjects) + foreach (var obj in potentialTransforms) { var d = Vector3.Distance(obj.position, transform.position); selectedPosition += obj.position; allObjectsBound.Encapsulate(obj.position); } - selectedPosition /= potentialObjects.Length; + selectedPosition /= potentialTransforms.Count; } } @@ -258,7 +281,7 @@ private void OnDrawGizmos() Gizmos.color = Color.red; Gizmos.DrawSphere(GetTargetPos(), 0.5f); - if (mode == Mode.Box) + if (mode == Mode.CameraTrap) { Vector2 delta = transform.position; Rect r = rect; diff --git a/Assets/OkapiKit/Scripts/Systems/Path.cs b/Assets/OkapiKit/Scripts/Systems/Path.cs index ea17628f..8438e332 100644 --- a/Assets/OkapiKit/Scripts/Systems/Path.cs +++ b/Assets/OkapiKit/Scripts/Systems/Path.cs @@ -31,8 +31,6 @@ public class Path : OkapiElement [SerializeField] private Color displayColor = Color.yellow; - private bool isSmooth => type == Type.Smooth; - private float primaryRadius; private Vector2 primaryDir; private float perpRadius; @@ -61,8 +59,9 @@ public void SetEditPoints(List inPoints) public bool isEditMode => editMode; public bool isWorldSpace => worldSpace; public bool isLocalSpace => !worldSpace; - public bool isClosed => closed; + public Type pathType => type; + protected override void Awake() { @@ -373,6 +372,32 @@ public void CenterPath() } } + public void ConvertToLocalSpace() + { + var matrix = transform.worldToLocalMatrix; + + for (int i = 0; i < points.Count; i++) + { + Vector4 pt = points[i]; + pt.w = 1; + + points[i] = matrix * pt; + } + } + + public void ConvertToWorldSpace() + { + var matrix = transform.localToWorldMatrix; + + for (int i = 0; i < points.Count; i++) + { + Vector4 pt = points[i]; + pt.w = 1; + + points[i] = matrix * pt; + } + } + public List GetPoints() { if ((!dirty) && (fullPoints == null)) return fullPoints; diff --git a/Assets/OkapiKit/Scripts/Triggers/Condition.cs b/Assets/OkapiKit/Scripts/Triggers/Condition.cs index 5c7571ff..7a258be8 100644 --- a/Assets/OkapiKit/Scripts/Triggers/Condition.cs +++ b/Assets/OkapiKit/Scripts/Triggers/Condition.cs @@ -41,6 +41,10 @@ public enum ValueType public Variable comparisonVariable; public bool percentageCompare; + // Objects to minimize GC allocs + static List potentialTransforms = new(); + static List gameObjects = new(); + public DataType GetDataType() { if (valueType == ValueType.Probe) return DataType.Boolean; @@ -218,11 +222,12 @@ public bool Evaluate(GameObject gameObject) { case Condition.ValueType.TagCount: { - var objects = HypertaggedObject.FindGameObjectsWithHypertag(tag); + gameObjects.Clear(); + HypertaggedObject.FindGameObjectsWithHypertag(tag, gameObjects); if (tagCountRangeEnabled) { currentValue = 0; - foreach (var obj in objects) + foreach (var obj in gameObjects) { if (Vector3.Distance(obj.transform.position, gameObject.transform.position) < tagCountRange) { @@ -232,10 +237,10 @@ public bool Evaluate(GameObject gameObject) } else { - currentValue = objects.Count; + currentValue = gameObjects.Count; } minValue = 0; - maxValue = objects.Count; + maxValue = gameObjects.Count; } break; case Condition.ValueType.WorldPositionX: @@ -296,8 +301,9 @@ public bool Evaluate(GameObject gameObject) if (sourceTransform) target = sourceTransform; else if (tag) { - var potentialObjects = gameObject.FindObjectsOfTypeWithHypertag(tag); - foreach (var obj in potentialObjects) + potentialTransforms.Clear(); + gameObject.FindObjectsOfTypeWithHypertag(tag, potentialTransforms); + foreach (var obj in potentialTransforms) { var d = Vector3.Distance(obj.position, gameObject.transform.position); if (d < currentValue) diff --git a/Assets/OkapiKit/Scripts/Triggers/Trigger.cs b/Assets/OkapiKit/Scripts/Triggers/Trigger.cs index d1a9860d..e640cb36 100644 --- a/Assets/OkapiKit/Scripts/Triggers/Trigger.cs +++ b/Assets/OkapiKit/Scripts/Triggers/Trigger.cs @@ -201,15 +201,18 @@ public virtual void ExecuteTrigger() foreach (var action in actions) { - if (action.action.isActionEnabled) + if (action.action != null) { - if (action.delay > 0) + if (action.action.isActionEnabled) { - StartCoroutine(ExecuteTriggerCR(action)); - } - else - { - action.action.Execute(); + if (action.delay > 0) + { + StartCoroutine(ExecuteTriggerCR(action)); + } + else + { + action.action.Execute(); + } } } } diff --git a/Assets/OkapiKit/package.json b/Assets/OkapiKit/package.json index 97453cf5..060de132 100644 --- a/Assets/OkapiKit/package.json +++ b/Assets/OkapiKit/package.json @@ -1,7 +1,7 @@ { "name": "com.videojogoslusofona.okapikit", "displayName": "OkapiKit", - "version": "1.15.0", + "version": "1.15.1", "unity": "2022.3", "description": "OkapiKit is a toolkit for creation of simple games without code.", "keywords": [ "kit" ], diff --git a/Assets/OkapiKitSamples/package.json b/Assets/OkapiKitSamples/package.json index 451bf27a..be178619 100644 --- a/Assets/OkapiKitSamples/package.json +++ b/Assets/OkapiKitSamples/package.json @@ -1,14 +1,14 @@ { "name": "com.videojogoslusofona.okapikit.samples", "displayName": "OkapiKit Samples", - "version": "1.15.0", + "version": "1.15.1", "unity": "2022.3", "description": "OkapiKit is a toolkit for creation of simple games without code. This package is just the samples for OkapiKit, they are not required to use Okapi Kit, and requires the Okapi Kit package.", "keywords": [ "okapi", "samples" ], "category": "samples", "dependencies": {}, "relatedPackages": { - "com.videojogoslusofona.okapikit": "1.15.0" + "com.videojogoslusofona.okapikit": "1.15.1" }, "author": { "name": "Videojogos ULHT", diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 728616f2..ed886caa 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,5 +1,13 @@ # Release Notes +## V1.15.1 + +- Trigger will no longer throw an exception when an action is null +- Removed some GC from condition evaluation +- Renamed "Box" mode on camera follow to "Camera Trap" +- Added Exponential Decay camera mode, which is like Feedback Loop, but frame independent +- Added function to convert a path to world/local space, plus the ability to do that in multiple objects at the same time + ## V1.15.0 - Added "any key" option to OnInput trigger