Skip to content

Commit d19c746

Browse files
authored
Merge pull request #371 from BenjaminNavarro/master
Add dynamic split screen demo
2 parents aca3ea4 + 5bdd178 commit d19c746

File tree

10 files changed

+1237
-0
lines changed

10 files changed

+1237
-0
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
extends Spatial
2+
3+
# Handle the motion of both players' camera as well as communication with the
4+
# SplitScreen shader to achieve the dynamic split screen effet
5+
#
6+
# Cameras are place on the segment joining the two players, either in the middle
7+
# if players are close enough or at a fixed distance if they are not.
8+
# In the first case, both cameras being at the same location, only the view of
9+
# the first one is used for the entire screen thus allowing the players to play
10+
# on a unsplit screen.
11+
# In the second case, the screen is split in two with a line perpendicular to the
12+
# segement joining the two players.
13+
#
14+
# The points of customization are:
15+
# max_separation: the distance between players at which the view starts to split
16+
# split_line_thickness: the thickness of the split line in pixels
17+
# split_line_color: color of the split line
18+
# adaptive_split_line_thickness: if true, the split line thickness will vary
19+
# depending on the distance between players. If false, the thickness will
20+
# be constant and equal to split_line_thickness
21+
22+
export(float) var max_separation = 20.0
23+
export(float) var split_line_thickness = 3.0
24+
export(Color, RGBA) var split_line_color = Color.black
25+
export(bool) var adaptive_split_line_thickness = true
26+
27+
onready var player1 = $'../Player1'
28+
onready var player2 = $'../Player2'
29+
onready var camera1: Camera = $'Viewport1/Camera1'
30+
onready var camera2: Camera = $'Viewport2/Camera2'
31+
onready var view: TextureRect = $'View'
32+
33+
34+
func _ready():
35+
_on_size_changed()
36+
_update_splitscreen()
37+
38+
get_viewport().connect("size_changed", self, "_on_size_changed")
39+
40+
view.material.set_shader_param('viewport1', $Viewport1.get_texture())
41+
view.material.set_shader_param('viewport2', $Viewport2.get_texture())
42+
43+
44+
func _process(_delta):
45+
_move_cameras()
46+
_update_splitscreen()
47+
48+
49+
func _move_cameras():
50+
var position_difference = _compute_position_difference_in_world()
51+
52+
var distance = clamp(_compute_horizontal_length(position_difference), 0, max_separation)
53+
54+
position_difference = position_difference.normalized() * distance
55+
56+
camera1.translation.x = player1.translation.x + position_difference.x / 2.0
57+
camera1.translation.z = player1.translation.z + position_difference.z / 2.0
58+
59+
camera2.translation.x = player2.translation.x - position_difference.x / 2.0
60+
camera2.translation.z = player2.translation.z - position_difference.z / 2.0
61+
62+
63+
func _update_splitscreen():
64+
var screen_size = get_viewport().get_visible_rect().size
65+
var player1_position = camera1.unproject_position(player1.translation) / screen_size
66+
var player2_position = camera2.unproject_position(player2.translation) / screen_size
67+
68+
var thickness
69+
if adaptive_split_line_thickness:
70+
var position_difference = _compute_position_difference_in_world()
71+
var distance = _compute_horizontal_length(position_difference)
72+
thickness = lerp(0, split_line_thickness, (distance - max_separation) / max_separation)
73+
thickness = clamp(thickness, 0, split_line_thickness)
74+
else:
75+
thickness = split_line_thickness
76+
77+
view.material.set_shader_param('split_active', _get_split_state())
78+
view.material.set_shader_param('player1_position', player1_position)
79+
view.material.set_shader_param('player2_position', player2_position)
80+
view.material.set_shader_param('split_line_thickness', thickness)
81+
view.material.set_shader_param('split_line_color', split_line_color)
82+
83+
84+
# Split screen is active if players are too far apart from each other.
85+
# Only the horizontal components (x, z) are used for distance computation
86+
func _get_split_state():
87+
var position_difference = _compute_position_difference_in_world()
88+
var separation_distance = _compute_horizontal_length(position_difference)
89+
return separation_distance > max_separation
90+
91+
92+
func _on_size_changed():
93+
var screen_size = get_viewport().get_visible_rect().size
94+
95+
$Viewport1.size = screen_size
96+
$Viewport2.size = screen_size
97+
view.rect_size = screen_size
98+
99+
view.material.set_shader_param('viewport_size', screen_size)
100+
101+
102+
func _compute_position_difference_in_world():
103+
return player2.translation - player1.translation
104+
105+
106+
func _compute_horizontal_length(vec):
107+
return Vector2(vec.x, vec.z).length()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
extends KinematicBody
2+
3+
# Moves the player
4+
5+
export(int, 1, 2) var player_id = 1
6+
export(float) var walk_speed = 20.0
7+
8+
9+
func _physics_process(_delta):
10+
var velocity = Vector3.ZERO
11+
velocity.z = -Input.get_action_strength("move_up_player" + str(player_id))
12+
velocity.z += Input.get_action_strength("move_down_player" + str(player_id))
13+
velocity.x = -Input.get_action_strength("move_left_player" + str(player_id))
14+
velocity.x += Input.get_action_strength("move_right_player" + str(player_id))
15+
16+
move_and_slide(velocity.normalized() * walk_speed)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# The project
2+
This sample project showcases an implementation of dynamic split screen, also called Voronoi split screen, using the [Godot engine](https://godotengine.org).
3+
4+
# Dynamic split screen
5+
A dynamic split screen system displays a single screen when the two players are close but a splitted view when they move apart.
6+
7+
The splitting line can take any angle depending on the players' position, so it won't be either vertical or horizontal.
8+
9+
This system was popularized by the Lego videogames.
10+
11+
# How it works
12+
Two cameras are placed inside two separate viewports and their texture, as well as some other parameters, are passed to a shader attached to a TextureRect filling the whole screen.
13+
14+
The `SplitScreen` shader, with the help of the `CameraController` script, chooses wich texture to display on each pixel to achieve the effect.
15+
16+
The cameras are placed on the segment joining the two players, either in the middle if they're close enough or at a fixed distance otherwise.
17+
18+
# How to use it
19+
Open and launch the project inside the Godot engine and then you can use WASD keys to move the first player and IJKL keys to move the second one.
20+
21+
The `Cameras` node has parameters to tune the distance at which the screen splits and also the width and color of the splitting line.
22+
23+
# Try it out
24+
An HTML5 export is testable [here](https://benjaminnavarro.github.io/godot_dynamic_split_screen/index.html).
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
shader_type canvas_item;
2+
render_mode unshaded;
3+
4+
uniform vec2 viewport_size; // size in pixels of the viewport. Cannot be access from the shader in GLES2
5+
uniform sampler2D viewport1 : hint_albedo;
6+
uniform sampler2D viewport2 : hint_albedo;
7+
uniform bool split_active; // true: split screen, false: use view1
8+
uniform vec2 player1_position; // position of player 1 un UV coordinates
9+
uniform vec2 player2_position; // position of player 2 un UV coordinates
10+
uniform float split_line_thickness; // width of the split boder
11+
uniform vec4 split_line_color; // color of the split border
12+
13+
14+
// from https://stackoverflow.com/questions/15276454/is-it-possible-to-draw-line-thickness-in-a-fragment-shader
15+
float distanceToLine(vec2 p1, vec2 p2, vec2 point) {
16+
float a = p1.y - p2.y;
17+
float b = p2.x - p1.x;
18+
return abs(a * point.x + b * point.y + p1.x * p2.y - p2.x * p1.y) / sqrt(a * a + b * b);
19+
}
20+
21+
void fragment() {
22+
vec3 view1 = texture(viewport1, UV).rgb;
23+
vec3 view2 = texture(viewport2, UV).rgb;
24+
25+
float width = viewport_size.x;
26+
float height = viewport_size.y;
27+
28+
if (split_active) {
29+
vec2 dx = player2_position - player1_position;
30+
float split_slope;
31+
32+
if (dx.y != 0.0) {
33+
split_slope = dx.x / dx.y;
34+
}
35+
else {
36+
split_slope = 100000.0; // High value (vertical split) if dx.y = 0
37+
}
38+
39+
vec2 split_origin = vec2(0.5, 0.5);
40+
vec2 split_line_start = vec2(0.0, height * ((split_origin.x - 0.0) * split_slope + split_origin.y));
41+
vec2 split_line_end = vec2(width, height * ((split_origin.x - 1.0) * split_slope + split_origin.y));
42+
float distance_to_split_line = distanceToLine(split_line_start, split_line_end, vec2(UV.x * width, UV.y * height));
43+
44+
// Draw split border if close enough
45+
if (distance_to_split_line < split_line_thickness) {
46+
COLOR = split_line_color;
47+
}
48+
else {
49+
float split_current_y = (split_origin.x - UV.x) * split_slope + split_origin.y;
50+
float split_player1_position_y = (split_origin.x - player1_position.x) * split_slope + split_origin.y;
51+
52+
// Check on which side of the split UV is and select the proper view
53+
if (UV.y > split_current_y) {
54+
if (player1_position.y > split_player1_position_y) {
55+
COLOR = vec4(view1, 1.0);
56+
}
57+
else {
58+
COLOR = vec4(view2, 1.0);
59+
}
60+
}
61+
else {
62+
if (player1_position.y < split_player1_position_y) {
63+
COLOR = vec4(view1, 1.0);
64+
}
65+
else {
66+
COLOR = vec4(view2, 1.0);
67+
}
68+
}
69+
}
70+
}
71+
else {
72+
COLOR = vec4(view1, 1.0);
73+
}
74+
}

0 commit comments

Comments
 (0)