Back
Runtime Editor
  • Graphs
  • Editor
  • Unity
Info

Role: Programmer

Category: Tools

Engine: Unity

Team Size: Solo

Time: 2 weeks

About

Runtime editor for car frame building. The latest build is mainly focused on graphs for building car frames, inspired by games like Dream Car Builder or Main Assembly. The main functionality for selecting and moving vertices could also be used on a level editor or on multiple other applications.

I took this challenge because I wanted to deepen my knowledge of graphs and the command pattern. The images below show some of the inspiration for this project.

Graph

My implementation of an undirectioned and unweighed graph.

Graph.cs Vertex.cs
Vertex Selection & Geometry
  • Vertex Selection: All vertices generate planes on their positions that always face the camera. On click, camera shoots a mouse ray that will intersect those planes. If the distance between the point of intersection and the actual vertices is small enough, then the mouse clicked on the vertex.
  • Transform Handles Selection: Intersecting mouse ray on plane and checking distance to projection on axis using dot product.
Code - Geometry Library
public static class Geometry
{
    public static Vertex VertexIntersectMouseRay(Camera camera, float range = 0.25f)
    {
        Ray mouseRay = camera.ScreenPointToRay(Input.mousePosition);
        Transform cameraTransform = camera.transform;
        foreach (Vertex vertex in GraphManager.Instance.Graph.Vertices)
        {
            Plane vertexPlane = new Plane(vertex.Position, vertex.Position + cameraTransform.up, vertex.Position + cameraTransform.right);
            
            if (vertexPlane.Raycast(mouseRay, out float distanceMouse))
            {
                Vector3 intersectMouse = mouseRay.GetPoint(distanceMouse);
                if ((intersectMouse - vertex.Position).magnitude <= range)
                {
                    return vertex;
                }
            }
        }
        return null;
    }
    
    public static Axis HoveringAxis(Camera camera, Vector3 position, float handleRange, out Vector3 xProjection, out Vector3 yProjection, out Vector3 zProjection)
    {
        Vector3 xPoint = position + Vector3.right;
        Vector3 yPoint = position + Vector3.up;
        Vector3 zPoint = position + Vector3.forward;
        Plane planeXZ = new Plane(position, xPoint, zPoint);
        Plane planeXY = new Plane(position, xPoint, yPoint);

        Ray mouseRay = camera.ScreenPointToRay(Input.mousePosition);

        Vector3 intersectXZ = IntersectPlaneWithRay(planeXZ, mouseRay);
        Vector3 intersectXY = IntersectPlaneWithRay(planeXY, mouseRay);
        Vector3 relativeXZ = intersectXZ - position;
        Vector3 relativeXY = intersectXY - position;
        
        Vector3 _xProjection = new Vector3(Vector3.Dot(intersectXZ - position, Vector3.right), 0f, 0f);
        Vector3 _zProjection = new Vector3(0f, 0f, Vector3.Dot(intersectXZ - position, Vector3.forward));
        Vector3 _yProjection = new Vector3(0f, Vector3.Dot(intersectXY - position, Vector3.up), 0f);

        if ((_xProjection - relativeXZ).magnitude <= handleRange && relativeXZ.magnitude <= EditorRenderer.Instance.HandleLength && Mathf.Sign(relativeXZ.x) >= 0)
        {
            xProjection = _xProjection;
            yProjection = _yProjection;
            zProjection = _zProjection;
            return Axis.X;
        }
        if ((_zProjection - relativeXZ).magnitude <= handleRange && relativeXZ.magnitude <= EditorRenderer.Instance.HandleLength && Mathf.Sign(relativeXZ.z) >= 0)
        {
            xProjection = _xProjection;
            yProjection = _yProjection;
            zProjection = _zProjection;
            return Axis.Z;
        }
        if ((_yProjection - relativeXY).magnitude <= handleRange && relativeXY.magnitude <= EditorRenderer.Instance.HandleLength && Mathf.Sign(relativeXY.y) >= 0)
        {
            xProjection = _xProjection;
            yProjection = _yProjection;
            zProjection = _zProjection;
            return Axis.Y;
        }
        xProjection = _xProjection;
        yProjection = _yProjection;
        zProjection = _zProjection;
        return Axis.None;
    }
    
    private static Vector3 IntersectPlaneWithRay(Plane plane, Ray ray)
    {
        if (!plane.Raycast(ray, out float distance)) return Vector3.zero;
        Vector3 intersectionPoint = ray.GetPoint(distance);
        return intersectionPoint;
    }
}
Code - Selectable Interface
public abstract class ISelectable
{
    public bool Selected;
    
    public abstract void OnSelect();
    public abstract void OnDeselect();
}

public class SelectableVertex : ISelectable
{
    private Vertex vertex;

    public SelectableVertex(Vertex vertex)
    {
        this.vertex = vertex;
    }
    
    public override void OnSelect()
    {
        Selected = true;
        vertex.Renderer.SetColor(Color.yellow);

        if (EditorController.Instance.SelectedVertices.Contains(vertex)) { return; }
        EditorController.Instance.SelectedVertices.Add(vertex);
    }

    public override void OnDeselect()
    {
        if (!EditorController.Instance.SelectedVertices.Contains(vertex)) { return; }
        
        Selected = false;
        vertex.Renderer.SetColor(Color.red);
    }
}
Mouse State Machine
MouseController.cs
State Transition Table
State ↓ Event → OnClickDown OnClickUp OnClickAndHover
Disabled Idling - -
Idling Disabled - Grabbing
Grabbing - Idling -
Disabled
Idling
Grabbing
Functionality
Function Behaviour Input
Pan Camera Pan Camera WASD
Select Vertex Select if vertex is clicked on. Deselect if click and no vertex in range Mouse Left
Camera Rotation Hold to yaw or pitch camera Mouse Right
Extrude Vertex Add vertex connected to selected vertex and select it E
Delete Vertex Remove vertex and all its edges from graph Delete
Add Edge Add edge between two selected vertices F
Remove Edge Remove edge between two selected vertices X
Singular/Multiple Selection Hold to switch from single to multiple vertices selection Left Ctrl
Select all Select all vertices Left Ctrl + A
Undo Execute undo command on top of stack Q
Undo (Command Pattern)
Command.cs CommandHandler.cs

Example of how the command pattern is being used to stack commands and then undo them.

When creating a vertex:

  • OnExecute: Save reference to created vertex.
  • OnUndo: Remove vertex referenced OnExecute.

When moving a vertex:

  • OnExecute: Save vertex position when dropped.
  • OnUndo: Move vertex to saved position.

Other Applications
Wall Building Tool (And other procedural mesh studies)

This wall builder tool is another project I made where I used some of the ideas from this project. In this project, I'm using procedural mesh generation for the walls and also implementing a different undo system based on the article below. For the meantime, this project is an editor tool but I plan to do a new iteration for an actual room builder and level editor in runtime inspired by The Sims Undo, the art of

© 2022 João Freire. All rights reserved.