Skip to content

Commit

Permalink
- Trigger will no longer throw an exception when an action is null
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
DiogoDeAndrade committed May 31, 2024
1 parent ef1d320 commit b3c10c0
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 74 deletions.
7 changes: 5 additions & 2 deletions Assets/OkapiKit/Scripts/Editor/CameraFollow2dEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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."));
}
Expand Down
149 changes: 108 additions & 41 deletions Assets/OkapiKit/Scripts/Editor/PathWaypointsEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace OkapiKit.Editor
{
[CustomEditor(typeof(Path))]
[CustomEditor(typeof(Path)), CanEditMultipleObjects]
public class PathEditor : OkapiBaseEditor
{
SerializedProperty propType;
Expand Down Expand Up @@ -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();
}
}
Expand Down
14 changes: 14 additions & 0 deletions Assets/OkapiKit/Scripts/Hypertag/HypertaggedObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ public static List<GameObject> FindGameObjectsWithHypertag(Hypertag[] tags)
{
List<GameObject> ret = new List<GameObject>();

FindGameObjectsWithHypertag(tags, ret);

return ret;
}

public static List<GameObject> FindGameObjectsWithHypertag(Hypertag[] tags, List<GameObject> ret)
{
foreach (var t in tags)
{
hypertaggedObjects.TryGetValue(t, out var list);
Expand All @@ -141,6 +148,13 @@ public static List<GameObject> FindGameObjectsWithHypertag(Hypertag tag)
{
List<GameObject> ret = new List<GameObject>();

FindGameObjectsWithHypertag(tag, ret);

return ret;
}

public static List<GameObject> FindGameObjectsWithHypertag(Hypertag tag, List<GameObject> ret)
{
hypertaggedObjects.TryGetValue(tag, out var list);
if (list != null)
{
Expand Down
47 changes: 35 additions & 12 deletions Assets/OkapiKit/Scripts/Systems/CameraFollow2d.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,6 +24,7 @@ public enum TagMode { Closest = 0, Furthest = 1, Average = 2 };

private new Camera camera;
private Bounds allObjectsBound;
private List<Transform> potentialTransforms = new();

protected override string Internal_UpdateExplanation()
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -105,7 +110,7 @@ void Start()
{
camera = GetComponent<Camera>();

if (mode == Mode.Box)
if (mode == Mode.CameraTrap)
{
float currentZ = transform.position.z;
Vector3 targetPos = GetTargetPos();
Expand All @@ -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;
}
}

Expand All @@ -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()
{
Expand Down Expand Up @@ -204,11 +225,13 @@ public Vector3 GetTargetPos()
else if (targetTag)
{
Vector3 selectedPosition = transform.position;
var potentialObjects = gameObject.FindObjectsOfTypeWithHypertag<Transform>(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)
Expand All @@ -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)
Expand All @@ -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;
}
}

Expand All @@ -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;
Expand Down
Loading

0 comments on commit b3c10c0

Please sign in to comment.