A "Front-Mirror" for Your 3D Character (or back)

Last week, I had this wild idea: what if my Bob’s Crystal game had a front-mirror? You know, a little preview of what’s ahead when moving toward the camera (or when inside huts and other tight spaces). Sounded brilliant.

So, I tinkered the whole weekend, lost some sanity in the process, but eventually came up with a solution… which I now present to you as a singleton-ish script for any 3D game character.

Will I use it in Bob’s Crystal? No clue. It feels a bit distracting, but I’ll let my testers decide whether it’s genius or just visual noise. In the meantime, I figured—why not share it? So, here’s how you can use it:

Create a new script, make it global, and call it GlobalPip.

In your scene, initialize it with your character’s instance:

GlobalPip.initialize_pip(bob_instance) —> bob_instance is your 3d instance

In your _process function, call:

GlobalPip.UpdatePIP()

Tweak GlobalPip for positioning and sizing to fit your needs.

That’s it! Enjoy, experiment, and let me know if you find cool ways to improve it.

Take care and have fun! :rocket:

extends Node
# -----------------------------------------------------
# res://scripts/bob_global_pipfunctions.gd - HEAD VIEW WITH LOGICAL ORGANIZATION
# Global name: GlobalPip
# -----------------------------------------------------

#------------------------------------------------------------------
# 1. CONFIGURATION CONSTANTS
#------------------------------------------------------------------
# Display settings
const PIP_WIDTH_PERCENT = 16.0      # Percentage of screen width
const PIP_HEIGHT_PERCENT = 9.0     # Percentage of screen height
const PIP_TOP_MARGIN = 20           # Pixels from top of screen
const PIP_SCALE_PERCENT = 25        # Legacy scaling constant

# Camera settings
const CAMERA_TILT_ANGLE = -0.25     # Default slight downward tilt for head view

# Camera modes
enum CameraMode {BEHIND, HEAD, SHOULDER}
var mode_names = ["BEHIND VIEW", "HEAD VIEW", "SHOULDER VIEW"]

#------------------------------------------------------------------
# 2. PROPERTIES
#------------------------------------------------------------------
# Node references
var bob = null                      # Reference to the Bob character
var main_camera: Camera3D = null    # Reference to the main camera
var pip_camera: Camera3D = null     # Camera used for PIP view
var pip_viewport: SubViewport = null # Viewport for the picture-in-picture
var pip_container: Control = null   # Container for the PIP display

# State variables
var pip_enabled = true              # Track if the feature is enabled
var camera_mode = CameraMode.HEAD   # Start with HEAD VIEW as default

#------------------------------------------------------------------
# 3. INITIALIZATION
#------------------------------------------------------------------
func _ready():
	print("GlobalPip singleton ready")

func initialize_pip(bob_instance):
	bob = bob_instance
	main_camera = get_node("/root/Bobgame/Camera3D")
	print("Main camera found: ", main_camera != null)
	
	create_scene_camera()
	create_pip_ui()

#------------------------------------------------------------------
# 4. CAMERA SETUP
#------------------------------------------------------------------
func create_scene_camera():
	pip_camera = Camera3D.new()
	pip_camera.name = "BobPIPCamera"
	pip_camera.current = false
	
	if main_camera:
		pip_camera.environment = main_camera.environment
		pip_camera.cull_mask = main_camera.cull_mask
		
	var game_node = get_node("/root/Bobgame")
	if game_node:
		game_node.add_child(pip_camera)
		print("Added camera to scene")
	else:
		print("ERROR: Couldn't find game node to add camera")

#------------------------------------------------------------------
# 5. UI CREATION AND MANAGEMENT
#------------------------------------------------------------------
func create_pip_ui():
	# Canvas layer setup
	var ui_layer = CanvasLayer.new()
	ui_layer.name = "PIPLayer"
	ui_layer.layer = 10
	add_child(ui_layer)
	
	# Container setup
	pip_container = Control.new()
	pip_container.name = "PIPContainer"
	pip_container.anchor_right = 1
	pip_container.anchor_bottom = 1
	ui_layer.add_child(pip_container)
	
	# ViewportContainer setup
	var viewport_container = SubViewportContainer.new()
	viewport_container.name = "PIPViewportContainer"
	
	# Set size based on screen percentage
	var viewport_size = get_viewport().size
	var pip_width = viewport_size.x * (PIP_WIDTH_PERCENT / 100.0)
	var pip_height = viewport_size.y * (PIP_HEIGHT_PERCENT / 100.0)
	
	viewport_container.size = Vector2(pip_width, pip_height)
	viewport_container.stretch = true
	
	# Position at top center
	viewport_container.anchor_left = 0.5
	viewport_container.anchor_right = 0.5
	viewport_container.anchor_top = 0.0
	viewport_container.anchor_bottom = 0.0
	viewport_container.position = Vector2(-pip_width/2, PIP_TOP_MARGIN)
	
	pip_container.add_child(viewport_container)
	
	# Create and setup viewport
	setup_pip_viewport(viewport_container, pip_width, pip_height)
	
	# Add border decoration
	add_pip_border(viewport_container, pip_width, pip_height)

func setup_pip_viewport(viewport_container, width, height):
	pip_viewport = SubViewport.new()
	pip_viewport.name = "PIPViewport"
	pip_viewport.size = Vector2(width, height)
	pip_viewport.handle_input_locally = false
	pip_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
	pip_viewport.transparent_bg = false
	pip_viewport.world_3d = get_viewport().world_3d
	
	viewport_container.add_child(pip_viewport)
	
	# Create camera for the viewport
	var viewport_camera = Camera3D.new()
	viewport_camera.name = "ViewportCamera"
	viewport_camera.current = true
	
	if main_camera:
		viewport_camera.environment = main_camera.environment
		viewport_camera.cull_mask = main_camera.cull_mask
		viewport_camera.fov = 85.0  # Wider FOV for better visibility
		
	pip_viewport.add_child(viewport_camera)

func add_pip_border(viewport_container, width, height):
	viewport_container.add_theme_constant_override("margin_right", 4)
	viewport_container.add_theme_constant_override("margin_top", 4)
	viewport_container.add_theme_constant_override("margin_left", 4)
	viewport_container.add_theme_constant_override("margin_bottom", 4)
	
	var panel = Panel.new()
	panel.name = "Border"
	panel.size = Vector2(width + 8, height + 8)
	panel.position = Vector2(-4, -4)
	panel.show_behind_parent = true
	panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
	viewport_container.add_child(panel)

#------------------------------------------------------------------
# 6. WINDOW RESIZE HANDLING
#------------------------------------------------------------------
func _notification(what):
	if what == NOTIFICATION_WM_SIZE_CHANGED:
		update_pip_size()

func update_pip_size():
	if not pip_viewport or not pip_container:
		return
		
	var viewport_container = pip_container.get_node_or_null("PIPViewportContainer")
	if not viewport_container:
		return
		
	var viewport_size = get_viewport().size
	var pip_width = viewport_size.x * (PIP_WIDTH_PERCENT / 100.0)
	var pip_height = viewport_size.y * (PIP_HEIGHT_PERCENT / 100.0)
	
	viewport_container.size = Vector2(pip_width, pip_height)
	pip_viewport.size = Vector2(pip_width, pip_height)
	viewport_container.position = Vector2(-pip_width/2, PIP_TOP_MARGIN)
	
	var panel = viewport_container.get_node_or_null("Border")
	if panel:
		panel.size = Vector2(pip_width + 8, pip_height + 8)

#------------------------------------------------------------------
# 7. USER INTERACTION
#------------------------------------------------------------------
func toggle_pip_visibility():
	if pip_container:
		pip_container.visible = !pip_container.visible
		pip_enabled = pip_container.visible
		print("PIP visibility toggled: ", pip_enabled)

#------------------------------------------------------------------
# 8. MAIN UPDATE LOOP
#------------------------------------------------------------------
func UpdatePIP():
	# Validation checks
	if not is_pip_valid():
		return
	
	# Check for camera mode switch
	if Input.is_action_just_pressed("ui_home") or Input.is_key_pressed(KEY_M):
		camera_mode = (camera_mode + 1) % mode_names.size()
		print("Switched to camera mode: ", mode_names[camera_mode])
	
	# Update camera position and orientation
	update_camera_position()
	
	# Copy transform to viewport camera
	var viewport_camera = pip_viewport.get_node_or_null("ViewportCamera")
	if viewport_camera:
		viewport_camera.global_transform = pip_camera.global_transform

#------------------------------------------------------------------
# 9. HELPER FUNCTIONS
#------------------------------------------------------------------
func is_pip_valid():
	if not pip_viewport or not pip_container or not pip_container.visible:
		return false
	
	if not bob or not is_instance_valid(bob):
		return false
	
	if not pip_viewport.get_node_or_null("ViewportCamera"):
		return false
	
	if not pip_camera or not is_instance_valid(pip_camera):
		print("PIP camera invalid, recreating")
		create_scene_camera()
		return false
		
	return true

func update_camera_position():
	var forward_dir = -Vector3(sin(bob.rotation.y), 0, cos(bob.rotation.y))
	var right_dir = Vector3(cos(bob.rotation.y), 0, sin(bob.rotation.y))
	
	match camera_mode:
		CameraMode.BEHIND:
			pip_camera.global_position = bob.global_position + Vector3(0, 2.0, 0) - forward_dir * 2.0
			pip_camera.look_at(bob.global_position + Vector3(0, 1.0, 0))
			
		CameraMode.HEAD:
			# Position at eye level with slight forward offset
			pip_camera.global_position = bob.global_position + Vector3(0, 0.8, 0) + forward_dir * 0.2
			
			# Match Bob's rotation with tilt
			pip_camera.global_rotation.y = bob.rotation.y
			pip_camera.global_rotation.x = CAMERA_TILT_ANGLE
			pip_camera.global_rotation.z = 0
			
		CameraMode.SHOULDER:
			pip_camera.global_position = bob.global_position + Vector3(0, 1.7, 0) - forward_dir * 0.7 + right_dir * 0.4
			pip_camera.look_at(bob.global_position + Vector3(0, 1.5, 0) + forward_dir * 1.0)

1 Like

i tried to collapse the code… but still climsy at these forum thingys :wink:

1 Like

3 Likes

It reminds me of need for speed hot pursuit, and I like it.

2 Likes

Played that a lot!!!.. :wink:

1 Like

Oh shoot… Totally missed that

main_camera = get_node(“/root/Bobgame/Camera3D”) was needed…
You’ll need to adjust line 45.

Oh, and line 63 too!

Update: I’m not using it after all. Less is more, and this thing is just way too distracting. Bob’s got enough to deal with already. :sweat_smile:

But hey… free code for you! :wink:

1 Like