As part of my bachelor thesis, we created Checkout Showdown as an application of our digital claymation research. The game is a 2-player competitive, so we intended to implement a splitscreen and some VFXs where it would cover one (or both) player's side of the screen. The WebGL demonstration below showcases the splitscreen solutions developed for the game.
In the demo, you control two cubes, one with WASD and the other with arrow keys. Move the cubes around to see how the screen splits. If the cube(s) is off-screen, press T to toggle the settings panel's visibility, showing you the scene fully or partially. Additionally, you can toggle the visibility of the splitscreen of 2 effects, dubbed "Stinky" and "Psychedelic".
The splitscreen visual is created using a shader function that integrates screen textures from three cameras: one that tracks both players (shared camera) and two that follow each player individually (individual cameras). The textures of the individual cameras are combined by splitting one side of the resulting render for each player. We opted to use a dynamic angled split instead of the static split so players can have a directional sense relative to each other. The angle is calculated using an up vector and a vector between the two players.
void LateUpdate()
{
//Calculating the split angle
//Because the view is top-down, Y-axis is irrelevant
_playersVector = new Vector2(_p1Pos.x, _p1Pos.z) - new Vector2(_p2Pos.x, _p2Pos.z);
_angleToPlayers = Vector2.SignedAngle(Vector2.up, _playersVector);
//The split line's vector is perpendicular to the vector between players => add 90 degrees
_splitAngle = _angleToPlayers + 90f;
//Set the angle into the shader
_splitMat.SetFloat("_SplitAngle", _splitAngle);
}
A game script controls which screen texture to display by calculating the distance between the players. When the players are some distance apart, the script tells the shader to use the individual cameras' textures instead of the shared camera's texture and vice versa. The switch happens suddenly, so we added a split line that grows and shrinks to ease the transition.
void LateUpdate()
{
...
//Calculating the distance between players
_playerDist = Vector3.Distance(_p1Pos, _p2Pos);
//Use the individual cameras' textures (1) if players are far apart (_splitDistance)
//Else use the shared camera's texture (0)
_isSplitScreen = _playerDist > _splitDistance;
_splitMat.SetFloat("_isSplitScreen", (_isSplitScreen ? 1 : 0));
//Calculating the split line's width
_splitLineWidth = Mathf.Clamp(_playerDist - _splitDistance, 0f, _maxLineWidth);
//Set the line width into the shader
_splitMat.SetFloat("_LineWidth", _splitLineWidth);
}
The visual effects also use the same working principle, but the split display is controlled by player IDs instead of player distance. We did this because game objects (such as stinky cheese and magic mushrooms) affect the player selectively, and we use player IDs to differentiate the affected players.
//int playerID: the player that called this function
//bool isStink: the stinky state of the player
public void PlayerStink(int playerID, bool isStink)
{
//Update the player's effect state
if(playerID == 0) _player1Stink = isStink;
if(playerID == 1) _player2Stink = isStink;
_bothPlayerStink = _player1Stink && _player2Stink;
//Determine if the stink vfx covers the screen (2f) or either player
float _stinkPlayer = _bothPlayerStink ? 2f :
_player1Stink ? 0f : 1f;
_mat?.SetFloat("_Player", _stinkPlayer);
//Turn off the vfx if neither player is stinky
_canvas.alpha = _player1Stink || _player2Stink ? 1f : 0f;
}