using System;
using Framework.Geometry;
using UnityEditor;
using UnityEngine;
namespace Framework.Actors.Platformer {
/// Allows the user to edit the abilities in the inspector window
[CustomEditor(typeof(Abilities))]
public class AbilitiesEditor : Editor {
/// Height of the diagram to illustrate the jump trajectory
private const float DiagramHeight = 150.0f;
/// Name of the buttons for selecting to number of possible jumps
private static readonly string[] JumpCountNames = {"None", "1", "2", "3", "4"};
/// Maximum running speed the developer can configure
private const float MaximumRunSpeed = 30.0f;
/// Maximum jump height the developer can configure
private const float MaximumJumpHeight = 10.0f;
#region struct GuiClippingScope
/// Scope that enter a GUI clipping area for its duration
private struct GuiClippingScope : IDisposable {
/// Initialized a new GUI clipping scope
/// Clipping rectangle the scope will set
public GuiClippingScope(Rect clippingRectangle) {
GUI.BeginClip(clippingRectangle);
}
/// Leaves the clipping rectangle again
public void Dispose() {
GUI.EndClip();
}
}
#endregion // struct GuiClippingScope
#region struct GlMatrixStackScope
/// Pushes the current GL matrix onto the stack for its duration
private struct GlMatrixStackScope : IDisposable {
/// Pushes the current GL matrix onto the stack
/// Not used
public GlMatrixStackScope(int dummy) {
GL.PushMatrix();
}
/// Pops the GL matrix from the stack again
public void Dispose() {
GL.PopMatrix();
}
}
#endregion // struct GlMatrixStackScope
#region struct GlCommandScope
/// Begins a GL command and ends it after the scope expires
private struct GlCommandScope : IDisposable {
/// Begins a GL command for the specified primitive topology
/// Primitive topology that will be drawn
public GlCommandScope(int mode) {
GL.Begin(mode);
}
/// Ends the GL command again
public void Dispose() {
GL.End();
}
}
#endregion // struct GlCommandScope
private bool areVelocityControlsExpanded;
/// Called when the editor is added to the inspector panel
protected void OnEnable() {
Shader shader = Shader.Find("Hidden/Internal-Colored");
this.material = new Material(shader);
}
/// Called when the editor is removed from the inspector panel
protected void OnDisable() {
DestroyImmediate(this.material);
}
/// Called when the inspector GUI is layouted or rendered
public override void OnInspectorGUI() {
Abilities abilities = target as Abilities;
if(abilities == null) {
Debug.LogError(
"Abilities editor shown for a component that was not an instance of " +
"the abilities class."
);
return;
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Jump Count");
abilities.JumpCount = GUILayout.SelectionGrid(abilities.JumpCount, JumpCountNames, 5);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Air Control");
abilities.AirControlFactor = GUILayout.HorizontalSlider(
abilities.AirControlFactor, 0.0f, 1.0f
);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Jump Height");
abilities.JumpHeight = GUILayout.VerticalSlider(
abilities.JumpHeight, MaximumJumpHeight, 0.0f,
GUILayout.MaxHeight(DiagramHeight)
);
drawJumpCurves(abilities);
}
if(abilities.SquatMovement != SquatMovementMode.Prohibit) {
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Sneak/Crawl Speed");
abilities.SneakingSpeed = GUILayout.HorizontalSlider(
abilities.SneakingSpeed, 0.0f, MaximumRunSpeed
);
}
}
EditorGUILayout.MinMaxSlider(
"Walk+Run Speed",
ref abilities.WalkingSpeed, ref abilities.RunningSpeed,
0.0f, MaximumRunSpeed
);
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Dash Speed");
abilities.DashingSpeed = GUILayout.HorizontalSlider(
abilities.DashingSpeed, 0.0f, MaximumRunSpeed
);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Speed for Floor Slide");
abilities.SpeedForFloorSlide= GUILayout.HorizontalSlider(
abilities.SpeedForFloorSlide, 0.0f, MaximumRunSpeed
);
}
this.areVelocityControlsExpanded = EditorGUILayout.Foldout(
this.areVelocityControlsExpanded, "Speed in Numbers", toggleOnLabelClick: true
);
if(this.areVelocityControlsExpanded) {
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.LabelField(" ", GUILayout.Width(10.0f));
EditorGUILayout.PrefixLabel("Walking Speed");
string walkingSpeedString = GUILayout.TextField(
abilities.WalkingSpeed.ToString("0.0"), 6
);
float.TryParse(walkingSpeedString, out abilities.WalkingSpeed);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.LabelField(" ", GUILayout.Width(10.0f));
EditorGUILayout.PrefixLabel("Running Speed");
string runningSpeedString = GUILayout.TextField(
abilities.RunningSpeed.ToString("0.0"), 6
);
float.TryParse(runningSpeedString, out abilities.RunningSpeed);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.LabelField(" ", GUILayout.Width(10.0f));
EditorGUILayout.PrefixLabel("Dash Speed");
string dashSpeedString = GUILayout.TextField(
abilities.DashingSpeed.ToString("0.0"), 6
);
float.TryParse(dashSpeedString, out abilities.DashingSpeed);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.LabelField(" ", GUILayout.Width(10.0f));
EditorGUILayout.PrefixLabel("Sneak Speed");
string sneakingSpeedString = GUILayout.TextField(
abilities.SneakingSpeed.ToString("0.0"), 6
);
float.TryParse(sneakingSpeedString, out abilities.SneakingSpeed);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.LabelField(" ", GUILayout.Width(10.0f));
EditorGUILayout.PrefixLabel("Speed for Floor Slide");
string floorSlideSpeedString = GUILayout.TextField(
abilities.SpeedForFloorSlide.ToString("0.0"), 6
);
float.TryParse(floorSlideSpeedString, out abilities.SpeedForFloorSlide);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.LabelField(" ", GUILayout.Width(10.0f));
EditorGUILayout.PrefixLabel("Maximum Jump Height");
string jumpHeightdString = GUILayout.TextField(
abilities.JumpHeight.ToString("0.0"), 6
);
float.TryParse(jumpHeightdString, out abilities.JumpHeight);
}
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Can Sprint");
abilities.CanSprint = EditorGUILayout.Toggle(abilities.CanSprint);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Can Dash");
abilities.CanDash = EditorGUILayout.Toggle(abilities.CanDash);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Can Dash In-Air");
abilities.CanAirDash = EditorGUILayout.Toggle(abilities.CanAirDash);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Can Wall-Jump");
abilities.CanWallJump = EditorGUILayout.Toggle(abilities.CanWallJump);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Slide down Walls");
abilities.CanWallSlide = EditorGUILayout.Toggle(abilities.CanWallSlide);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Wall Hang Mode");
abilities.WallHanging = (LedgeHangMode)EditorGUILayout.EnumPopup(abilities.WallHanging);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Pole Hang Mode");
abilities.PoleHanging = (LedgeHangMode)EditorGUILayout.EnumPopup(abilities.PoleHanging);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Can Squat");
abilities.CanSquat = EditorGUILayout.Toggle(abilities.CanSquat);
}
using(new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Squat Movement Mode");
abilities.SquatMovement = (SquatMovementMode)EditorGUILayout.EnumPopup(
abilities.SquatMovement
);
}
//base.OnInspectorGUI();
}
/// Draws the jump curves currently set up by the developer
/// Abilities the editor is letting the developer edit
private void drawJumpCurves(Abilities abilities) {
Rect controlRectangle = GUILayoutUtility.GetRect(10, 1000, DiagramHeight, DiagramHeight);
if(Event.current.type == EventType.Repaint) {
using(new GuiClippingScope(controlRectangle)) {
// Adjust control rectangle for inside clipping scope
controlRectangle = new Rect(0.0f, 0.0f, controlRectangle.width, controlRectangle.height);
// World coordinate area that will be displayed within the illustration
Rect viewRectangle;
{
float maximumJumpOffVelocity = Trajectory.GetJumpOffVelocity(MaximumJumpHeight);
viewRectangle = new Rect(
0.0f,
MaximumJumpHeight,
Trajectory.GetTimeInFlight(maximumJumpOffVelocity) * MaximumRunSpeed,
-MaximumJumpHeight // flip in Y (control coordinates are Y-down)
);
}
GL.PushMatrix();
this.material.SetPass(0);
// Fill the background
glFillRectangle(controlRectangle, inspectorControlBackgroundColor);
// Draw the jump trajectory for a jump from walking speed
glDrawJumpTrajectory(
controlRectangle, viewRectangle,
abilities.WalkingSpeed, abilities.JumpHeight, Color.cyan
);
// Draw the jump trajectory for a jump from running speed
glDrawJumpTrajectory(
controlRectangle, viewRectangle,
abilities.RunningSpeed, abilities.JumpHeight, Color.green
);
GL.PopMatrix();
} // GUI clipping rectangle
}
}
///
/// Draws the jump trajectory for a given horizontal velocity and jump height
///
/// Pixel coordinates covered by the control
/// Rectangle of the world coordinates in the view
/// Horizontal velocity of the jumping actor
/// Height of the jump the actor will perform
/// Color of the jump trajectory line
private static void glDrawJumpTrajectory(
Rect controlRectangle, Rect viewRectangle,
float horizontalVelocity, float jumpHeight, Color color
) {
using(new GlCommandScope(GL.LINE_STRIP)) {
GL.Color(color);
// Jump-off velocity required to reach the desired height
float initialVelocity = Trajectory.GetJumpOffVelocity(jumpHeight);
// Draw the jump trajectory curve by stepping 1 unit in world coordinates.
// This will guarantee optimal resolution.
int startX = Mathf.FloorToInt(controlRectangle.xMin);
int endX = Mathf.CeilToInt(controlRectangle.xMax);
for(int pixelX = startX; pixelX < endX; ++pixelX) {
// Calculate the time at which the actor would reach this X coordinate
float time;
{
float windowX = (pixelX - controlRectangle.x) / controlRectangle.width;
float worldX = windowX * viewRectangle.width + viewRectangle.x;
time = worldX / horizontalVelocity;
}
// Calculate at which height in the parabola trajectory the actor should be now
float y = Trajectory.GetParabolaY(initialVelocity, time);
// Convert the world Y coordinate into pixel Y coordinate
float pixelY;
{
float windowY = (y - viewRectangle.y) / viewRectangle.height;
pixelY = windowY * controlRectangle.height + controlRectangle.y;
}
GL.Vertex3((float)pixelX, pixelY, 0.0f);
// If the actor has fallen back to the floor, stop drawing
if(y < 0.0f) {
break;
}
}
}
}
/// Issues the GL commands to fill a rectangle
/// Coordinates of the filled rectangle
/// Color the rectangle will be filled with
private static void glFillRectangle(Rect rectangle, Color color) {
using(new GlCommandScope(GL.QUADS)) {
GL.Color(color);
GL.Vertex3(rectangle.xMin, rectangle.yMin, 0.0f);
GL.Vertex3(rectangle.xMax, rectangle.yMin, 0.0f);
GL.Vertex3(rectangle.xMax, rectangle.yMax, 0.0f);
GL.Vertex3(rectangle.xMin, rectangle.yMax, 0.0f);
}
}
/// Issues the GL commands to draw a rectangle
/// Coordinates of the rectangle
/// Color of the rectangle's outline
private static void glDrawRectangle(Rect rectangle, Color color) {
using(new GlCommandScope(GL.LINE_STRIP)) {
GL.Color(color);
GL.Vertex3(rectangle.xMin, rectangle.yMin, 0.0f);
GL.Vertex3(rectangle.xMax, rectangle.yMin, 0.0f);
GL.Vertex3(rectangle.xMax, rectangle.yMax, 0.0f);
GL.Vertex3(rectangle.xMin, rectangle.yMax, 0.0f);
}
}
/// Background color the inspector panel itself
private static Color inspectorPanelBackgroundColor {
get {
if(EditorGUIUtility.isProSkin) {
return (Color)new Color32(56, 56, 56, 255);
} else {
return (Color)new Color32(224, 224, 224, 255);
}
}
}
/// Background color of controls in the inspector panel
private static Color inspectorControlBackgroundColor {
get {
if(EditorGUIUtility.isProSkin) {
return (Color)new Color32(76, 76, 76, 255);
} else {
return (Color)new Color32(224, 224, 224, 255);
}
}
}
/// Material that is used to draw in the inspector diagram
private Material material;
}
} // namespace Framework.Actors.Platformer