Toybox Camera: an orthographic Camera3D with self-determined movement limits based on level geometry

SUMMARY

After a very long time and many different attempts at implementing this behavior, I have (finally! :partying_face:) developed a Camera3D that can determine where it’s allowed to move while showing minimal OOB area.

Huge thanks to @japonbaligi for helping me get the basic math of this down; I couldn’t have done it without their help.

MAIN FEATURES

  • A camera that uses the built-in physics collision system to forbid movement that would show any area outside a camera-aligned (rather than axis-aligned) rectangle that circumscribes four or more predetermined points on a flat XZ plane.
  • Automatically correction of the camera’s position if it strays outside of this rectangle via zooming and/or rotation.
  • Prediction of whether input rotations will result in the camera going OOB, and forbidding such rotation if detected.
  • Each bound will always maintain its relative position to the camera, i.e. no matter what the camera’s rotation is, the North bound will always be at the top of the screen.

DISCLAIMERS AND POSSIBLE IMPROVEMENTS

  • This has not actually been tested for rooms defined by more than four corners, or rooms that are non-rectangular.
  • The math that determines the positions of the camera’s bounds does not scale with an arbitrary Fixed_Rotate_Power. I had hoped to do this, but I’d also like to move on with my life.
  • The camera does not utilize its attachment to the player character after setting its initial position. My next goal is to make it able to roam freely, but snap back to the player character’s position when they want to move.
  • I actually didn’t love how the predictive forbidding of rotation looked, so I bypass it in my own project. It could probably be made to look/work better.
  • The camera can only have one of two different sizes, so it cannot be zoomed arbitrarily between these sizes. This should be possible by reconfiguring the current code, but it falls outside the scope of my intended behavior.
  • The bounds aren’t translated inward when the Camera3D is zoomed out. This is how I like it, but might not be desirable for others if they want to use this, though it should be fairly simple to re-enable that behavior.

ESSENTIAL SCENE SETUP

The Marker3Ds should be manually aligned to their respective corners of the room. This tree does not include details of the room itself, only the essentials to make the code that follows it work!

Each of the nodes that emits a signal is emitting the tree_entered() signal toward the Initialize_Link method of the main script, passing itself as the only argument. If you’d like to try this for yourself, make sure you consider the order of the bounds and markers in the tree hierarchy, since that will determine their order in the receiver array! And don’t worry about the errors for the CollisionShape3Ds; those are because I didn’t assign them shapes in the editor, since they’ll be assigned shapes by the script anyway.

CharacterBody3D:                 πŸ“œ Camera_Overworld
β”œβ”€β”€ CollisionShape3D:               Collider_Origin
└── CollisionShape3D:               Collider_Front
    └── Camera3D
β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”

Node3D
β”œβ”€β”€ StaticBody3D:                   Camera_Bounds
β”‚   β”œβ”€β”€ CollisionShape3D:     ⚠️ πŸ›œ East
β”‚   β”‚   └── MeshInstance3D
β”‚   β”œβ”€β”€ CollisionShape3D:     ⚠️ πŸ›œ North
β”‚   β”‚   └── MeshInstance3D
β”‚   β”œβ”€β”€ CollisionShape3D:     ⚠️ πŸ›œ Wednesday
β”‚   β”‚   └── MeshInstance3D
β”‚   └── CollisionShape3D:     ⚠️ πŸ›œ East
β”‚       └── MeshInstance3D
β”œβ”€β”€ Node3D:                         Camera_Bound_Points
β”‚   β”œβ”€β”€ Marker3D:                πŸ›œ Point_Northeast
β”‚   β”œβ”€β”€ Marker3D:                πŸ›œ Point_Northwest
β”‚   β”œβ”€β”€ Marker3D:                πŸ›œ Point_Southwest
β”‚   β”œβ”€β”€ Marker3D:                πŸ›œ Point_Northeast
β”‚   └── Marker3D:                πŸ›œ Point_Floor
β”œβ”€β”€ Camera_Overworld          🎬 πŸ“œ
└── (the level itself!)

CODE USED

(Please see reply to this post, since I’ve hit the character limit).

DEMONSTRATION

1 Like
using Godot;
using System;

public partial class MathHandler : Node
{
    public static readonly float Pi = (float) Math.PI;
    public static readonly float DegToRad = (float) Math.PI / 180;
    public static readonly float RadFull = (float) Math.PI * 2;

    /// <summary>
    /// Increases or decreases an input number, then runs modulus.
    /// </summary>
    /// <param name="start">The input number.</param>
    /// <param name="length">The amount by which to modulus divide.</param>
    /// <param name="amount">The amount by which to change the input number.</param>
    public static int AddThenModulus(int start, int length, int amount = 1)
    {
        int cycles_up = 0;
        if (amount < 0)
        {
            cycles_up = Math.Abs(amount) / length + 1;
            if (Math.Abs(amount) % length == 0)
            {
                cycles_up --;
            }
        }
        
        return (length * cycles_up + start + amount) % length;
    }

    public static Variant[] Array_RotateUpward(Variant[] input, int amount = 1)
    {
        amount %= input.Length;
        if (amount == 0) return input;

        input = Array_ReverseSection(input, 0, input.Length);
        input = Array_ReverseSection(input, 0, amount);
        input = Array_ReverseSection(input, amount, input.Length);
        
        return input;
    }

    public static Variant[] Array_RotateDownward(Variant[] input, int amount = 1)
    {
        amount %= input.Length;
        if (amount == 0) return input;

        input = Array_ReverseSection(input, 0, amount);
        input = Array_ReverseSection(input, amount, input.Length);
        input = Array_ReverseSection(input, 0, input.Length);

        return input;
    }

    private static Variant[] Array_ReverseSection(Variant[] input, int start, int section)
    {
        section --;
        while (start < section)
        {
            (input[section], input[start]) = (input[start], input[section]);
            start++;
            section --;
        }

        return input;
    }

    public static bool Point_IsWithinPolygon(Vector2 point, Vector2[] corners)
    {
        //https://stackoverflow.com/a/14998816
        bool result = false;
        int j = corners.Length - 1;

        for (int i = 0; i < corners.Length; i++)
        {
            if (corners[i].Y < point.Y && corners[j].Y >= point.Y || 
                corners[j].Y < point.Y && corners[i].Y >= point.Y)
            {
                if (corners[i].X + (point.Y - corners[i].Y) /
                   (corners[j].Y - corners[i].Y) *
                   (corners[j].X - corners[i].X) < point.X)
                {
                    result = !result;
                }
            }
            j = i;
        }
        return result;
    }
}
using System;
using System.Collections.Generic;
using Godot;

public partial class Camera_Overworld : CharacterBody3D
{
	//VARIABLES THAT LINK NODES TOGETHER
	private CollisionShape3D myCollider_Front;
	private Camera3D myCamera;
	private List<CollisionShape3D> yourBounds_Colliders = [];
	private List<Marker3D> your_RoomPoints = [];

	//VARIABLES THAT DESCRIBE THE FIXED PROPERTIES OF THE ROOM
	private Vector3[] Fixed_Corners_AxisAligned = new Vector3[2];
	private readonly float BakedIn_Bound_Thickness = 1; //this is the thickness of the room's walls
	private readonly float BakedIn_Wall_Height = 7; //this is the verticle distance from the floor of the room to the top of its walls

	//VARIABLES THAT DESCRIBE THE FIXED PROPERTIES OF THE CAMERA
	[Export] private bool State_Debug_Collision = false;
	private BaseMaterial3D[] Debug_Mesh_Materials = new BaseMaterial3D[2];
	private readonly float Dependent_Height_Above_Wall = 17.5f; //this depends on the sizes of the camera, and is whatever minimum height you need to prevent occlusion of objects
	private float Dependent_GlobalPosition_Y; //this will be calculated from the above variable on initialization
	[Export] private float Fixed_Angle = 45 * MathHandler.Pi; //this is the angle of the camera's Rotation.X
	[Export] private int[] Fixed_Sizes = [16, 48]; //the zoomed-in and zoomed-out Sizes of the camera
	[Export] private float Fixed_Zoom_Seconds = 0.5f; //the amount of time it takes to zoom in or out
	[Export] private byte Fixed_Rotate_Power = 3; //the higher this number, the smaller each rotation on input will be
	private float Fixed_Rotate_Rads; //the magnitude of each rotation on input
	[Export] private float Fixed_Rotate_Seconds = 0.5f; //the amount of time it takes to rotate
	[Export] private int[] Fixed_Speeds = [512, 1024, 2048]; //the speeds at which the camera moves
	[Export] private float Fixed_Move_FromOOB_Speed = 32; //the speed at which the camera autocorrects if it goes OOB

	//VARIABLES THAT DESCRIBE THE INITIAL STATE OF THE CAMERA
	[Export] private bool Init_Zoomed = true;
	[Export] private byte Init_Rotate = 0;
	[Export] private byte Init_Speed = 2;

	//VARIABLES THAT DESCRIBE THE CURRENT STATE OF THE ROOM
	private Vector3 Current_Bound_Centroid; //the centroid of the camera-aligned rectangle the circumscribes each Marker3D
	private readonly Vector3[,] CurrentAndFuture_Edges = new Vector3[3, 4]; //the GlobalPositions of the middles of the line segments connecting each corner of the circumscribed rectangle, and what they will be on rotation in either direction
	private Vector2[][] CurrentAndFuture_Corners_ActualBounds = [new Vector2[4], new Vector2[4], new Vector2[4], new Vector2[4]]; //the GlobalPositions of each CollisionShape3D used to keep the camera in bounds, and what they will be on rotation in either direction

	//VARIABLES THAT DESCRIBE THE CURRENT STATE OF THE CAMERA
	private bool[] States_Zoom = [false, false]; //[whether-zooming, whether-zoomed-in]
	private float Counter_Zoom = 0;
	private bool[] States_Rotate = [false, false]; //[whether-rotating, whether-rotating-counterclockwise]
	private float[] Points_Rotate = new float[2];
	private float Counter_Rotate = 0;
	private int Current_Rotate = 0;
	private byte State_Move_Speed;
	private bool State_Move_FromOOB = true; //I don't remember why this starts off true
	private Vector3 Current_Move_FromOOB_Dir;
	private Vector3 MostRecent_Position;

	public override void _Ready()
	{
		CheckErrors(0);

		Initialize_Link("");

		Fixed_Rotate_Rads = 2 * MathHandler.Pi / MathF.Pow(2, Fixed_Rotate_Power);

		CallDeferred(MethodName.Initialize_Nodes);
	}

    public override void _Process(double delta)
    {
		if (!State_Move_FromOOB)
		{
			if (OS.IsDebugBuild() && Input.IsActionJustPressed("debug_view_toggle"))
			{
				State_Debug_Collision = !State_Debug_Collision;

				foreach (CollisionShape3D bound in yourBounds_Colliders)
				{
					if (State_Debug_Collision) bound.GetNode<MeshInstance3D>("MeshInstance3D").SetSurfaceOverrideMaterial(0, Debug_Mesh_Materials[0]);
					else bound.GetNode<MeshInstance3D>("MeshInstance3D").SetSurfaceOverrideMaterial(0, Debug_Mesh_Materials[1]);
				}
			}

			if (Input.IsActionJustPressed("cam_speed_slow") ^ Input.IsActionJustPressed("cam_speed_medium") ^ Input.IsActionJustPressed("cam_speed_fast"))
			{
				byte new_speed_maybe;

				if (Input.IsActionJustPressed("cam_speed_slow"))   new_speed_maybe = 0;
				else if (Input.IsActionJustPressed("cam_speed_medium")) new_speed_maybe = 1;
				else new_speed_maybe = 2;

				if (new_speed_maybe != State_Move_Speed)
				{
					State_Move_Speed = new_speed_maybe;
				}
			}
		}
    }

	public override void _PhysicsProcess(double delta)
	{
		if (State_Move_FromOOB) Logic_Move((float) delta);
		else
		{
			//this short-circuits to disable rotation if currently zooming. I'm not sure if that's necessary but I implemented it early because I like it
			if (Logic_Zoom((float) delta) || Logic_Rotate((float) delta)) return;
			Logic_Move((float) delta);
		}
	}

	private void CheckErrors(byte step)
	{
		switch (step)
		{
			case 0:

				if (Fixed_Rotate_Power <= 1)
				{
					GD.PushError("Rotation power must be greater than 1.");
					GetTree().Quit();
				}
				else if (Fixed_Rotate_Power > 3)
				{
					GD.PushWarning("Bound logic has not been properly configured for Rotation_Power > 3 and does not scale from current logic."); //see Possible Improvements note 2
				}
				if (Init_Rotate >= (float) Math.Pow(2, Fixed_Rotate_Power))
				{
					GD.PushError("Starting rotation increment is greater than range of sections.");
					GetTree().Quit();
				}

				break;

			case 1:

				if (yourBounds_Colliders.Count != 4)
				{
					GD.PushError("One or more linked CollisionShape3Ds have not been set."); 
					GetTree().Quit();
				}
				if (your_RoomPoints.Count != 5)
				{
					GD.PushWarning("Linked Marker3Ds indicate a non-quadrilateral room, which has not been tested."); //see Possible Improvements note 1
				}
				
				float running = your_RoomPoints[0].GlobalPosition.Y;
				for (byte i = 1; i < your_RoomPoints.Count - 1; i ++)
				{
					if (your_RoomPoints[i].GlobalPosition.Y != running)
					{
						GD.PushError("One or more members of Camera_Bound_Points have unequal Y positions."); //I didn't check what would happen if you broke this rule
						GetTree().Quit();
					}
				}

				break;
		}
	}

	private void Initialize_Link(NodePath thisNode)
	{
		//this is a little janky but at a small number of nodes it's fine
		if (thisNode == "")
		{
			myCollider_Front = GetNode<CollisionShape3D>("Collider_Front");
			myCamera = myCollider_Front.GetNode<Camera3D>("Camera3D");
		}
		else
		{
			string node_path = Owner.GetPath() + thisNode.ToString()[2..];

			if (node_path.Contains("Camera_Bounds"))
			{
				yourBounds_Colliders.Add(GetNode<CollisionShape3D>(node_path));
			}
			else if (node_path.Contains("Camera_Bound_Points"))
			{
				your_RoomPoints.Add(GetNode<Marker3D>(node_path));
			}
			else
			{
                GD.PushError("An unexpected Node is emitting a signal toward Initialize_Link.");
				GetTree().Quit();
			}
		}
	}

	private Vector3[] Initialize_Markers()
	{
		//find the smallest axis-aligned rectangle that contains each of your_RoomPoints
		Vector3 coord_min = new(float.MaxValue, your_RoomPoints[0].GlobalPosition.Y, float.MaxValue);
		Vector3 coord_max = new(float.MinValue, your_RoomPoints[0].GlobalPosition.Y, float.MinValue);

		//doing it this way futureproofs it in case there's ever a non-rectangular room, just change the i maximum
		for (byte i = 0; i < 4; i ++)
		{
			for (byte j = 0; j < 3; j += 2)
			{
				coord_min[j] = Math.Min(your_RoomPoints[i].GlobalPosition[j], coord_min[j]);
				coord_max[j] = Math.Max(your_RoomPoints[i].GlobalPosition[j], coord_max[j]);
			}
		}

		return [coord_min, coord_max];
	}

	private void Initialize_Nodes()
	{
		CheckErrors(1);

		//initialize the debug materials here since I can't figure out how to construct BaseMaterial3D without cloning it
		Debug_Mesh_Materials[0] = yourBounds_Colliders[0].GetNode<MeshInstance3D>("MeshInstance3D").GetSurfaceOverrideMaterial(0).Duplicate() as BaseMaterial3D;
		Debug_Mesh_Materials[1] = Debug_Mesh_Materials[0];
		Debug_Mesh_Materials[1].Transparency = BaseMaterial3D.TransparencyEnum.Disabled;

		//initialize axis-aligned rectangle for room geometry
		Fixed_Corners_AxisAligned = Initialize_Markers();

		//initialize zoom
		States_Zoom[1] = Init_Zoomed;
		if (Init_Zoomed) myCamera.Size = Fixed_Sizes[0];
		else myCamera.Size = Fixed_Sizes[1];

		//initialize rotation
		myCamera.Rotation = new Vector3(-Fixed_Angle, 0, 0);
		GlobalRotation = new Vector3(0, Init_Rotate * Fixed_Rotate_Rads, 0);
		Current_Rotate = Init_Rotate;

		//initialize movement
		State_Move_Speed = Init_Speed;

		//reparent the node to the first instance of the player character node
		Reparent(Player_Overworld_CharacterBody3D.ourPlayers[0]); //just replace this with you player character object

		//calculate the fixed Y height
		Dependent_GlobalPosition_Y = your_RoomPoints[0].GlobalPosition.Y + Dependent_Height_Above_Wall;

		//move pivot point directly above player, then adjust
		Vector3 pos_player = GetParent<Player_Overworld_CharacterBody3D>().GlobalPosition;		
		MostRecent_Position = GlobalPosition = new Vector3
		(
			pos_player.X,
			Dependent_GlobalPosition_Y,
			pos_player.Z
		);

		//set the distance of the front-facing collider
		myCollider_Front.Position = new Vector3(0, 0, (BakedIn_Wall_Height + Dependent_Height_Above_Wall) / MathF.Tan(Fixed_Angle));

		Update_Bounds();
	}

	private void Update_Bounds()
	{
		//amount by which to increase the corner a bound looks at so that its position relative to the camera is constant
			//e.g. at GlobalRotation = 0, yourBounds_Colliders[0] should go from corner 3 to corner 0
			//while at GlobalRotation = Pi / 2, that same bound should go from corner 0 to corner 1
		byte[] corners = new byte[2];

		//declare tuple variables
		int corner_advance_factor;
		Vector3[] current_camaligned_corners;
		float[] sin_s;
		float[] cos_s;
		Vector3 camaligned_margin;

		//declare adjustment variables
		//nothing ever touches the Y and Z components of scale, so they can just be set once here
		Vector3 scale = new(1f / 16f, 1, 1f / 16f);
		float rotation_y;
		BoxShape3D new_shape;
		Vector2[] returned_oobs = new Vector2[4];

		//loop through the bound function and run it stripped down for the next clockwise and counterclockwise rotations
		for (int i = -1; i < 2; i ++)
		{
			//NOTE THAT SOME MATH IS DONE IN THE TUPLE FUNCTION WITH corner_advance_factor BEFORE RETURNING ITS FINAL VALUE
			(corner_advance_factor, current_camaligned_corners, sin_s, cos_s, camaligned_margin) = Updater_Calculate_Variables(i);

			//set the transforms of the bounds on the camera-aligned rectangle, then adjust by relevant margin
			for (byte j = 0; j < 4; j ++)
			{
				corners[0] = (byte) ((3 + j + corner_advance_factor) % 4);
				corners[1] = (byte) ((corners[0] + 1) % 4);

				//translate is a messy switch statement so it gets siloed in its own method
				CurrentAndFuture_Edges[i + 1, j] = Update_Bound_Position(i, j, corners, corner_advance_factor, current_camaligned_corners, camaligned_margin, sin_s, cos_s);

				//everything in this block only applies to current bounds (i == 0)
				if (i == 0)
				{
					//determine scale and rotation
					scale.X = current_camaligned_corners[corners[0]].DistanceTo(current_camaligned_corners[corners[1]]);
					if (j % 2 == 0) rotation_y = GlobalRotation.Y + MathHandler.Pi / 2;
					else rotation_y = GlobalRotation.Y;

					//assign scale and rotation
					new_shape = new() {Size = scale};
					yourBounds_Colliders[j].Shape = new_shape;
					yourBounds_Colliders[j].GetNode<MeshInstance3D>("MeshInstance3D").Scale = scale;
					yourBounds_Colliders[j].GlobalRotation = new Vector3(0, rotation_y, 0);

					yourBounds_Colliders[j].GlobalPosition = CurrentAndFuture_Edges[1, j];
				}

				//update corners used in OOB detection
				if (j == 3)
				{
					CurrentAndFuture_Corners_ActualBounds[i + 1] = Update_CoordsOOB((byte) (i + 1), corner_advance_factor);
					if (i == 0)	CurrentAndFuture_Corners_ActualBounds[3] = Update_CoordsOOB(3, corner_advance_factor);
				}
			}
		}
	}

	private Tuple<int, Vector3[], float[], float[], Vector3> Updater_Calculate_Variables(int rotational_shift)
	{
		int total_slices = (int) Math.Pow(2, Fixed_Rotate_Power);
		int simulated_current_rotate = MathHandler.AddThenModulus(Current_Rotate, total_slices, rotational_shift);

		int quarter_slices = (int) Math.Pow(2, Fixed_Rotate_Power - 2);
		int corner_advance_factor = simulated_current_rotate / quarter_slices;

		//IF THIS ISN'T CALLED BEFORE DIAGONAL ADVANCEMENT IT WILL SHIFT THE ORDER OF THE RETURN ARRAY
		Vector3[] current_camaligned_corners = Update_CameraAlignedBoundingBox(corner_advance_factor, rotational_shift);

		//diagonal advancement 
        //(from my own comments: can you write a more detailed comment explaining why the hell this needs to happen next time)
		corner_advance_factor += Math.Sign(simulated_current_rotate % quarter_slices);

		//set trigonometry variables
			//camaligned_margin IS THE THING THAT MAKES THIS ENTIRE OPERATION WORK
			//https://forum.godotengine.org/t/mathematically-find-orthographic-camera-bound-knowing-size-angle-and-position/142503
		float[] angles =
		[
			GlobalRotation.Y + rotational_shift * Fixed_Rotate_Rads + MathHandler.Pi / 2,
			GlobalRotation.Y + rotational_shift * Fixed_Rotate_Rads
		];
		float[] sin_s = [MathF.Sin(angles[0]), MathF.Sin(angles[1])];
		float[] cos_s = [MathF.Cos(angles[0]), MathF.Cos(angles[1])];
		Vector3 camaligned_margin = new		
		(
			myCamera.Size / 2 * GetViewport().GetVisibleRect().Size.X / GetViewport().GetVisibleRect().Size.Y,
			0,
			(myCamera.Size / 2 - (Dependent_GlobalPosition_Y - current_camaligned_corners[0].Y) * MathF.Cos(Fixed_Angle)) / MathF.Sin(Fixed_Angle)
		);
		
		return Tuple.Create(corner_advance_factor, current_camaligned_corners, sin_s, cos_s, camaligned_margin);
	}

	private Vector3[] Update_CameraAlignedBoundingBox(int corner_advance_factor, int rotational_shift)
	{
		Current_Bound_Centroid = new
		(
			(Fixed_Corners_AxisAligned[0].X + Fixed_Corners_AxisAligned[1].X) / 2,
			your_RoomPoints[0].GlobalPosition.Y,
			(Fixed_Corners_AxisAligned[0].Z + Fixed_Corners_AxisAligned[1].Z) / 2
		);

		float inner_half_width = (Fixed_Corners_AxisAligned[1].X - Fixed_Corners_AxisAligned[0].X) / 2;
		float inner_half_height = (Fixed_Corners_AxisAligned[1].Z - Fixed_Corners_AxisAligned[0].Z) / 2;

		float cos_theta = (float) Math.Cos(GlobalRotation.Y + Fixed_Rotate_Rads * rotational_shift);
		float sin_theta = (float) Math.Sin(GlobalRotation.Y + Fixed_Rotate_Rads * rotational_shift);

		Vector3 axis_circum_width = new(cos_theta, 0, sin_theta);
		Vector3 axis_circum_height = new(-sin_theta, 0, cos_theta);

		float circum_half_width = inner_half_width * MathF.Abs(cos_theta) + inner_half_height * MathF.Abs(sin_theta);
		float circum_half_height = inner_half_width * MathF.Abs(sin_theta) + inner_half_height * MathF.Abs(cos_theta);

		Variant[] sort_buffer =
        [
            Current_Bound_Centroid + circum_half_width * axis_circum_width - circum_half_height * axis_circum_height,
            Current_Bound_Centroid - circum_half_width * axis_circum_width - circum_half_height * axis_circum_height,
            Current_Bound_Centroid - circum_half_width * axis_circum_width + circum_half_height * axis_circum_height,
            Current_Bound_Centroid + circum_half_width * axis_circum_width + circum_half_height * axis_circum_height,
        ];

		//always keep the same corners in the same index
		//rotate the array downward for each 90 degrees of camera rotation
		sort_buffer = MathHandler.Array_RotateDownward(sort_buffer, corner_advance_factor);

		return
		[
			(Vector3) sort_buffer[0],
			(Vector3) sort_buffer[1],
			(Vector3) sort_buffer[2],
			(Vector3) sort_buffer[3]
		];
	}

	private Vector3 Update_Bound_Position(int access_modifier, byte bound_index, byte[] corners_in_question, int corner_advance_factor, Vector3[] current_camaligned_corners, Vector3 camaligned_margin, float[] sin_s, float[] cos_s)
	{
		Vector3 bound_camaligned_position = (current_camaligned_corners[corners_in_question[0]] + current_camaligned_corners[corners_in_question[1]]) / 2;
		Vector3 transl_with_margin = Vector3.Zero;

		if (State_Debug_Collision) bound_camaligned_position.Y = Dependent_GlobalPosition_Y - Dependent_Height_Above_Wall;
		else 					   bound_camaligned_position.Y = Dependent_GlobalPosition_Y;

		byte simulated_current_rotate = (byte) MathHandler.AddThenModulus(Current_Rotate, (int) Math.Pow(2, Fixed_Rotate_Power), access_modifier);

		//translate bound by margin so it stops camera from moving past edges of room
        //the following are from my own comments:
		//for whatever reason the advance factor seems to always be correct on a certain result of if_even() for diagonal
			//NOTE the differences in the if statements are subtle! check the second comparison operators
		//2026_08_13 ALSO this is not scaleable indefinitely, it breaks at Fixed_Rotate_Power > 3
			//but I don't see a need for that to happen and would like to move on with my life
			//I'm sure it's possible and might have to do with readjusting corner_advance_factor, but again, my life
		//just don't send them inward if the camera is zoomed out I guess
		if (States_Zoom[1])
		{
			switch (bound_index)
			{
				case 0:

					transl_with_margin.X = camaligned_margin.X * sin_s[0] * -1;
					if (simulated_current_rotate % 2 == 0 && corner_advance_factor % 2 == 0) transl_with_margin.Z = camaligned_margin.X * cos_s[0];
					else transl_with_margin.Z = camaligned_margin.X * cos_s[0] * -1;
					break;

				case 1:

					float adjustment = Dependent_Height_Above_Wall - BakedIn_Wall_Height - BakedIn_Bound_Thickness / 4;
					transl_with_margin.X = (camaligned_margin.Z + adjustment) * sin_s[1];
					if (simulated_current_rotate % 2 == 0 && corner_advance_factor % 2 != 0) transl_with_margin.Z = (camaligned_margin.Z + adjustment) * cos_s[1] * -1;
					else transl_with_margin.Z = (camaligned_margin.Z + adjustment) * cos_s[1];
					break;

				case 2:

					transl_with_margin.X = camaligned_margin.X * sin_s[0];
					transl_with_margin.Z = camaligned_margin.X * cos_s[0];
					break;

				case 3:

					transl_with_margin.Z = camaligned_margin.Z * cos_s[1] * -1;
					if (simulated_current_rotate % 2 == 0 && corner_advance_factor % 2 == 0) transl_with_margin.X = camaligned_margin.Z * sin_s[1];
					else transl_with_margin.X = camaligned_margin.Z * sin_s[1] * -1;
					break;
			}
		}

		return bound_camaligned_position + transl_with_margin;
	}

	private Vector2[] Update_CoordsOOB(byte access_index, int corner_advance_factor)
	{
		/*
		access_index == 0 --> adjacent clockwise rotation
		access_index == 1 --> current rotation
		access_index == 2 --> adjacent counterclockwise rotation
		access_index == 3 --> current rotation shrunk toward centroid
		*/

		byte[] virtual_is = new byte[2];
		Vector2[] rotated_bound_positions = new Vector2[2];;
		bool was_access_index_3 = access_index == 3;
		if (was_access_index_3) access_index = 1;

		//quarter_increment is the same as Current_Rotate but only for traveling through one quarter of the circle
			//(from my own comments: this will break if you use the raw increment instead of the quarter and I don't quite recall why, oops)
		int quarter_increment = MathHandler.AddThenModulus(Current_Rotate, (int) Math.Pow(2, Fixed_Rotate_Power), access_index - 1) % (int) Math.Pow(2, Fixed_Rotate_Power - 2);
		float analyte_angle = - quarter_increment * Fixed_Rotate_Rads;
		Vector2 translate_values = Vector2.Zero;
		int adj_sign_z;

		Vector2[] actual_corners = new Vector2[4];
		for (byte i = 0; i < 4; i ++)
		{
			virtual_is[0] = i;
			virtual_is[1] = (byte) ((i + 1) % 4);

			//find the coordinates of each bound adjacent to the current corner, rotated around the centroid by the current angle
			rotated_bound_positions =
			[
				new Vector2
				(
					MathF.Cos(analyte_angle) * (CurrentAndFuture_Edges[access_index, virtual_is[0]].X - Current_Bound_Centroid.X) - MathF.Sin(analyte_angle) * (CurrentAndFuture_Edges[access_index, virtual_is[0]].Z - Current_Bound_Centroid.Z) + Current_Bound_Centroid.X,
					MathF.Sin(analyte_angle) * (CurrentAndFuture_Edges[access_index, virtual_is[0]].X - Current_Bound_Centroid.X) + MathF.Cos(analyte_angle) * (CurrentAndFuture_Edges[access_index, virtual_is[0]].Z - Current_Bound_Centroid.Z) + Current_Bound_Centroid.Z
				),
				new Vector2
				(
					MathF.Cos(analyte_angle) * (CurrentAndFuture_Edges[access_index, virtual_is[1]].X - Current_Bound_Centroid.X) - MathF.Sin(analyte_angle) * (CurrentAndFuture_Edges[access_index, virtual_is[1]].Z - Current_Bound_Centroid.Z) + Current_Bound_Centroid.X,
					MathF.Sin(analyte_angle) * (CurrentAndFuture_Edges[access_index, virtual_is[1]].X - Current_Bound_Centroid.X) + MathF.Cos(analyte_angle) * (CurrentAndFuture_Edges[access_index, virtual_is[1]].Z - Current_Bound_Centroid.Z) + Current_Bound_Centroid.Z
				)
			];

			//find the camera-aligned distance between those rotated positions,
			//and use it to translate the actual position of a bound to its edge a.k.a the current corner
			switch ((i + corner_advance_factor) % 4)
			{
				case 0:
				case 2:

					translate_values.X = (rotated_bound_positions[1].Y - rotated_bound_positions[0].Y) * MathF.Sin(analyte_angle);
					translate_values.Y = (rotated_bound_positions[1].Y - rotated_bound_positions[0].Y) * MathF.Cos(analyte_angle);
					break;

				case 1:
				case 3:

					adj_sign_z = 1;
					if (quarter_increment != 0) adj_sign_z = -1;

					translate_values.X = (rotated_bound_positions[1].X - rotated_bound_positions[0].X) * MathF.Cos(analyte_angle);
					translate_values.Y = (rotated_bound_positions[1].X - rotated_bound_positions[0].X) * MathF.Sin(analyte_angle) * adj_sign_z;
					break;
			}

			actual_corners[i] = new Vector2
			(
				CurrentAndFuture_Edges[access_index, i].X + translate_values.X,
				CurrentAndFuture_Edges[access_index, i].Z + translate_values.Y
			);

			//shrunk down set is smaller to make the camera have to move further to get back in bounds than it did to escape
			if (was_access_index_3)
			{
				actual_corners[i] = new Vector2
				(
					actual_corners[i].X - BakedIn_Bound_Thickness / 2 * Math.Sign(actual_corners[i].X - Current_Bound_Centroid.X),
					actual_corners[i].Y - BakedIn_Bound_Thickness / 2 * Math.Sign(actual_corners[i].Y - Current_Bound_Centroid.Z)
				);
			}
		}

		return actual_corners;		
	}

	private bool Logic_Zoom(float delta)
	{
		if(!States_Zoom[0] && Input.IsActionJustPressed("cam_zoom_toggle"))
		{
			States_Zoom[0] = true;
			return true;
		}
		else if (States_Zoom[0])
		{
			if (Counter_Zoom >= 1)
			{
				States_Zoom[0] = false;
				States_Zoom[1] = !States_Zoom[1];
				Counter_Zoom = 0;

				Update_Bounds();
				return false;
			}
			else
			{
				Counter_Zoom += delta / Fixed_Zoom_Seconds;

				if (States_Zoom[1]) myCamera.Size = Mathf.Lerp(Fixed_Sizes[0], Fixed_Sizes[1], Counter_Zoom);
				else myCamera.Size = Mathf.Lerp(Fixed_Sizes[1], Fixed_Sizes[0], Counter_Zoom);

				return true;
			}
		}

		return false;
	}

	private bool Logic_Rotate(float delta)
	{
		if (!States_Rotate[0] && (Input.IsActionJustPressed("cam_rot_left") || Input.IsActionJustPressed("cam_rot_right")))
		{
			//simulate the desired rotation and check it against
			byte simulation = 0;
			if (Input.IsActionJustPressed("cam_rot_left")) simulation = 2;
			
			if (true)
			//if (Check_IsWithinBounds(simulation))
			{
				States_Rotate[0] = true;
				States_Rotate[1] = Input.IsActionJustPressed("cam_rot_left");

				Points_Rotate[0] = GlobalRotation.Y;
				if (States_Rotate[1])
				{
					Points_Rotate[1] = GlobalRotation.Y + Fixed_Rotate_Rads;
					Current_Rotate = MathHandler.AddThenModulus(Current_Rotate, (int) Math.Pow(2, Fixed_Rotate_Power));
				}
				else
				{
					Points_Rotate[1] = GlobalRotation.Y - Fixed_Rotate_Rads;
					Current_Rotate = MathHandler.AddThenModulus(Current_Rotate, (int) Math.Pow(2, Fixed_Rotate_Power), -1);
				}

				Enable_Collision(false);
				return true;
			}
			else return false;
			
		}
		else if (States_Rotate[0])
		{
			if (Counter_Rotate >= 1)
			{
				States_Rotate[0] = false;
				Counter_Rotate = 0;

				Enable_Collision(true);

				Update_Bounds();
				return false;
			}
			else
			{
				Counter_Rotate += delta / Fixed_Rotate_Seconds;

				float rot_current;
				if (States_Rotate[1]) {rot_current = Mathf.Lerp(Points_Rotate[0], Points_Rotate[1], Counter_Rotate);}
				else				  {rot_current = Mathf.Lerp(Points_Rotate[1], Points_Rotate[0], 1 - Counter_Rotate);}

				GlobalRotation = new Vector3
				(
					GlobalRotation.X,
					rot_current,
					GlobalRotation.Z
				);

				return true;
			}
		}

		return false;
	}

	private void Enable_Collision(bool enabled)
	{
		//(from my own comments: do both sets of CollisionShape3Ds so this method can feel more like it's contributing)
		GetNode<CollisionShape3D>("Collider_Origin").Disabled = !enabled;
		GetNode<CollisionShape3D>("Collider_Front").Disabled = !enabled;

		foreach(CollisionShape3D bound in yourBounds_Colliders)
		{
			bound.Disabled = !enabled;
		}
	}

	private void Logic_Move(float delta)
	{
		if (State_Move_FromOOB)
		{
			//push toward center of room if OOB and override movement until done
			//just do it at a constant speed because to lerp it you'd need to find out which bound is closest
			Velocity = Current_Move_FromOOB_Dir * Fixed_Move_FromOOB_Speed;
			MoveAndSlide();

			//check whether it's done
			if (Check_IsWithinBounds(3))
			{
				State_Move_FromOOB = false;
				Enable_Collision(true);
			}
		}
		else if (Check_IsWithinBounds(1) || State_Debug_Collision)
		{
			//get normalized input and convert to fixed speed
			Vector2 input = Input.GetVector("cam_move_left", "cam_move_right", "cam_move_forward", "cam_move_backward").Normalized() * Fixed_Speeds[State_Move_Speed] * delta;
			//convert prior result into relative direction
			Velocity = new Vector3(input.X, 0, input.Y) * Basis.FromEuler(new Vector3(0, -Rotation.Y, 0));

			MoveAndSlide();
		}
		else
		{
			State_Move_FromOOB = true;
			Enable_Collision(false);

			//find the direction the pivot point needs to move
			Vector3 centroid_xz = new(Current_Bound_Centroid.X, Dependent_GlobalPosition_Y, Current_Bound_Centroid.Z);
			Current_Move_FromOOB_Dir = (centroid_xz - GlobalPosition).Normalized();
		}
	}

	private bool Check_IsWithinBounds(byte array_row)
	{
		//I don't actually remember why it starts with State_Move_FromOOB = true
		if (yourBounds_Colliders.Count == 0)
		{
			return true;
		}

		//this is actually a fixed variable but it's not stored
		float dist_to_lookpoint_xz = (BakedIn_Wall_Height + Dependent_Height_Above_Wall) / MathF.Tan(Fixed_Angle);

		//simulate rotation of camera around pivot point
		Vector2 check_pos;
		float theta_difference;
		if (array_row == 0 || array_row == 2)
		{
			theta_difference = Fixed_Rotate_Rads * - (array_row - 1);
			check_pos = new Vector2
			(
				MathF.Cos(theta_difference) * (myCamera.GlobalPosition.X - GlobalPosition.X) - MathF.Sin(theta_difference) * (myCamera.GlobalPosition.Z - GlobalPosition.Z) + GlobalPosition.X,
				MathF.Sin(theta_difference) * (myCamera.GlobalPosition.X - GlobalPosition.X) + MathF.Cos(theta_difference) * (myCamera.GlobalPosition.Z - GlobalPosition.Z) + GlobalPosition.Z 
			);
		}
		else
		{
			if (array_row != 1 && array_row != 3)
			{
				GD.PushError("Camera_Overworld: Parameter array_row of Check_IsWithinBounds must be from 0 to 3.");
				GetTree().Quit();
			}

			theta_difference = 0;
			check_pos = new Vector2(myCamera.GlobalPosition.X, myCamera.GlobalPosition.Z);
		}

		Vector2 check_lookingat_pos = new
		(	
			GlobalPosition.X + dist_to_lookpoint_xz * MathF.Cos(GlobalRotation.Y + theta_difference),
			GlobalPosition.Z + dist_to_lookpoint_xz * MathF.Sin(GlobalRotation.Y + theta_difference)
		);

		//not a huge fan of this being such a simple check since it's always preferable to use myCamera if possible
		//but it works well enough that it can be left alone for now
		if (check_lookingat_pos.DistanceTo(CurrentAndFuture_Corners_ActualBounds[array_row][1]) < check_lookingat_pos.DistanceTo(CurrentAndFuture_Corners_ActualBounds[array_row][3]))
		{
			check_pos = new Vector2 (GlobalPosition.X, GlobalPosition.Z);
		}

		return MathHandler.Point_IsWithinPolygon(check_pos, CurrentAndFuture_Corners_ActualBounds[array_row]);
	}
}
1 Like

glad to help. happy coding!