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!
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)