Split screen
Two SubViewports side by side with one camera each, both looking at the same World3D. Each pane follows its own cube.
The editor above is running this project. Change a line and it reloads.
Godot classes used
SubViewportSubViewportContainerCamera3D
The code
scripts/main.gd
extends Node
const SPEED: float = 6.0
var blue: Node3D
var green: Node3D
var blue_camera: Camera3D
var green_camera: Camera3D
var elapsed: float = 0.0
var manual: bool = false
func _ready() -> void:
_add_environment()
_add_light()
_add_arena()
blue = _add_runner(Color('#8da5f3'))
green = _add_runner(Color('#8eef97'))
var row: HBoxContainer = HBoxContainer.new()
row.add_theme_constant_override('separation', 6)
add_child(row)
row.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
blue_camera = _add_pane(row)
green_camera = _add_pane(row)
_add_hud()
_place_cameras()
print('two SubViewports share one World3D, wasd drives the blue cube and the arrows the green one')
func _process(delta: float) -> void:
elapsed += delta
var left: Vector3 = Vector3(
float(Input.is_key_pressed(KEY_D)) - float(Input.is_key_pressed(KEY_A)),
0.0,
float(Input.is_key_pressed(KEY_S)) - float(Input.is_key_pressed(KEY_W))
)
var right: Vector3 = Vector3(
float(Input.is_key_pressed(KEY_RIGHT)) - float(Input.is_key_pressed(KEY_LEFT)),
0.0,
float(Input.is_key_pressed(KEY_DOWN)) - float(Input.is_key_pressed(KEY_UP))
)
if left != Vector3.ZERO or right != Vector3.ZERO:
manual = true
if manual:
blue.position += left.normalized() * SPEED * delta
green.position += right.normalized() * SPEED * delta
else:
blue.position = Vector3(cos(elapsed * 0.7) * 7.0, 0.7, sin(elapsed * 0.7) * 7.0)
green.position = Vector3(cos(-elapsed * 0.5 + 2.2) * 4.5, 0.7, sin(-elapsed * 0.5 + 2.2) * 4.5)
blue.rotation.y = elapsed * 1.4
green.rotation.y = -elapsed * 1.4
_place_cameras()
func _place_cameras() -> void:
_aim(blue_camera, blue, Vector3(0, 4.5, 8))
_aim(green_camera, green, Vector3(6, 5.5, -6))
func _aim(camera: Camera3D, target: Node3D, offset: Vector3) -> void:
camera.position = target.position + offset
camera.look_at(target.position, Vector3.UP)
func _add_pane(row: HBoxContainer) -> Camera3D:
var container: SubViewportContainer = SubViewportContainer.new()
container.stretch = true
container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
container.size_flags_vertical = Control.SIZE_EXPAND_FILL
row.add_child(container)
var pane: SubViewport = SubViewport.new()
pane.render_target_update_mode = SubViewport.UPDATE_ALWAYS
pane.msaa_3d = Viewport.MSAA_4X
container.add_child(pane)
var camera: Camera3D = Camera3D.new()
pane.add_child(camera)
camera.current = true
return camera
func _add_runner(color: Color) -> Node3D:
var mesh: BoxMesh = BoxMesh.new()
mesh.size = Vector3(1.4, 1.4, 1.4)
var material: StandardMaterial3D = StandardMaterial3D.new()
material.albedo_color = color
material.roughness = 0.4
var runner: MeshInstance3D = MeshInstance3D.new()
runner.mesh = mesh
runner.material_override = material
add_child(runner)
return runner
func _add_arena() -> void:
var ground_mesh: PlaneMesh = PlaneMesh.new()
ground_mesh.size = Vector2(50, 50)
var ground_material: StandardMaterial3D = StandardMaterial3D.new()
ground_material.albedo_color = Color('#2a2f3d')
var ground: MeshInstance3D = MeshInstance3D.new()
ground.mesh = ground_mesh
ground.material_override = ground_material
add_child(ground)
var pillar_mesh: BoxMesh = BoxMesh.new()
pillar_mesh.size = Vector3(1.2, 4, 1.2)
var pillar_material: StandardMaterial3D = StandardMaterial3D.new()
pillar_material.albedo_color = Color('#fc7f7f')
pillar_material.roughness = 0.6
for i in 14:
var angle: float = TAU * i / 14.0
var pillar: MeshInstance3D = MeshInstance3D.new()
pillar.mesh = pillar_mesh
pillar.material_override = pillar_material
pillar.position = Vector3(cos(angle), 0, sin(angle)) * 17.0 + Vector3(0, 2, 0)
add_child(pillar)
func _add_hud() -> void:
var hud: CanvasLayer = CanvasLayer.new()
hud.layer = 2
add_child(hud)
var view: Vector2 = get_viewport().get_visible_rect().size
for item in [{ text = 'P1 wasd', color = Color('#8da5f3'), x = 40.0 }, { text = 'P2 arrows', color = Color('#8eef97'), x = view.x * 0.5 + 40.0 }]:
var label: Label = Label.new()
label.text = item.text
label.add_theme_font_size_override('font_size', 24)
label.add_theme_color_override('font_color', item.color)
label.position = Vector2(item.x, 30)
hud.add_child(label)
func _add_environment() -> void:
var sky_material: ProceduralSkyMaterial = ProceduralSkyMaterial.new()
sky_material.sky_top_color = Color(0.05, 0.06, 0.12)
sky_material.sky_horizon_color = Color(0.14, 0.16, 0.24)
sky_material.ground_bottom_color = Color(0.02, 0.02, 0.04)
sky_material.ground_horizon_color = Color(0.1, 0.11, 0.16)
var sky: Sky = Sky.new()
sky.sky_material = sky_material
var env: Environment = Environment.new()
env.background_mode = Environment.BG_SKY
env.sky = sky
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
env.ambient_light_color = Color(0.44, 0.48, 0.6)
env.ambient_light_energy = 0.7
var world: WorldEnvironment = WorldEnvironment.new()
world.environment = env
add_child(world)
func _add_light() -> void:
var light: DirectionalLight3D = DirectionalLight3D.new()
light.rotation_degrees = Vector3(-70, -35, 0)
light.light_energy = 1.2
light.shadow_enabled = true
add_child(light)