Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: Dynamic Split Screen  (Read 7180 times)

0 Members and 1 Guest are viewing this topic.

thePyro_13

  • Full Member
  • ***
  • Posts: 156
    • View Profile
Dynamic Split Screen
« on: March 12, 2014, 11:27:02 am »
Here's a quick bit of example code on what I call Dynamic Split Screens, this is the effect I first encountered in one of the newer Lego Star Wars games.

I was inspired to give this a shot after reading this post about non axis aligned viewports.
I had already written this post but managed to lose it(I compulsively hit refresh :(). So please tell me if their is anything I missed or should explain in more detail. :(

The goal is to smoothly split and re-merge the two split views based on player locations.
This provides a good middle ground between games that don't use split screens at all(like Super Smash Bro.'s) and games that use static split rectangles, like most console games.

We don't want to split into predefined rectangles, because this can disorient players when their avatar jumps to the opposite side of the screen, we also want to avoid the same problem when re-merging the views.

The solution is to have the two views dynamically generated based on the players relative positions to one another. Also we make sure that the views can cleanly rotate around one other if the players end up circling one another.

This explanation is much worse than my original one, so here's a video showing it off:


Finally, here's the code:
I use SFML for most of the work, and Thor for some vector maths.
The map.png I'm using is just a big image of the world from the original Legend of Zelda.
//===============================================================
// Dynamic Split Screen Example for SFML(http://sfml-dev.org/)
//
// The code in this file was developed by Steven Pilkington.
// More information about this example can be
// found on my blog(http://cyangames.wordpress.com/)
//
// I'm putting this in the public domain, so have fun with it.
//
//                                                                                              Date: 12/03/2014
//===============================================================

#include "SFML/Graphics.hpp"

#include "Thor/Vectors.hpp"

//Set the window resolution.
const int WIDTH = 800;
const int HEIGHT = 600;

//Ditermine if the views should be split based on the difference between both players positions.
bool shouldSplit(sf::Vector2f position1, sf::Vector2f position2);

//Calculate the camera position for the object at position1, moving the camera in the direction of position2.
sf::Vector2f viewPosition(sf::Vector2f position1, sf::Vector2f position2);

int main()
{
        //Create window.
        sf::RenderWindow window;
        window.create(sf::VideoMode(WIDTH, HEIGHT), "Dynamic Split Screen");

        //Create two player sprites.
        sf::Vector2f playerSize(10, 10);
        sf::RectangleShape p1(playerSize), p2(playerSize);

        p1.setFillColor(sf::Color::Blue);
        p2.setFillColor(sf::Color::Red);

        //Multiply our movements by this to control player movement speed.
        const float PLAYERSPEED = .1f;

        //Create background.
        //We're using a big image of the origional Legend of Zelda game world as our background.
        sf::Texture mapTex;
        mapTex.loadFromFile("map.png");
        sf::Sprite map(mapTex);
        //Move the background sprite so that the world origin is somewhere in the middle of the map.
        map.setPosition(-sf::Vector2f(map.getLocalBounds().width / 2.f, map.getLocalBounds().height / 2.f));

        //Create our views.
        //One for each player, player ones view is also used for both players when the views are merged.
        sf::View p1View = window.getView();
        sf::View p2View = window.getView();

        //View movement speed.
        const float VIEWSPEED = .2f;

        //Rendertexture is used to render player twos view of the world.
        sf::RenderTexture p2Tex;
        p2Tex.create(WIDTH, HEIGHT);
        sf::Sprite p2Sprite;

       

        //We use a large rectangle to draw the devider between the two camaras in slit screen mode.
        //We also use this to blend the two views together.
        sf::RectangleShape eraser;
        eraser.setOutlineColor(sf::Color::Black);
        eraser.setFillColor(sf::Color::Transparent);
        eraser.setOutlineThickness(2);
        eraser.setSize(sf::Vector2f(WIDTH*2, HEIGHT*2));

        //Start main loop.
        while(window.isOpen())
        {
                //Handle events.
                sf::Event e;
                if(window.pollEvent(e))
                {
                        if(e.type == sf::Event::Closed)
                                window.close();
                }

                //User controls.
                sf::Vector2f move;
                //Player one input.
                if(sf::Keyboard::isKeyPressed(sf::Keyboard::W))
                        move.y += -1;

                if(sf::Keyboard::isKeyPressed(sf::Keyboard::S))
                        move.y += 1;

                if(sf::Keyboard::isKeyPressed(sf::Keyboard::A))
                        move.x += -1;

                if(sf::Keyboard::isKeyPressed(sf::Keyboard::D))
                        move.x += 1;

                p1.move(move * PLAYERSPEED);

                //Reset the move vector.
                move = sf::Vector2f();
                //Player two input.
                if(sf::Keyboard::isKeyPressed(sf::Keyboard::Up))
                        move.y += -1;

                if(sf::Keyboard::isKeyPressed(sf::Keyboard::Down))
                        move.y += 1;

                if(sf::Keyboard::isKeyPressed(sf::Keyboard::Left))
                        move.x += -1;

                if(sf::Keyboard::isKeyPressed(sf::Keyboard::Right))
                        move.x += 1;

                p2.move(move * PLAYERSPEED);

                //Now we need do decide if we split our views.
                bool singleView = true;
                //Ideal position is the location that the camera wants to be in.
                sf::Vector2f idealPos;
       
                if(shouldSplit(p1.getPosition(), p2.getPosition()))
                {                      
                        //We want to split, so we calculate the ideal position for each camera using viewPosition().
                        singleView = false;
                        idealPos = viewPosition(p1.getPosition(), p2.getPosition());
                        p1View.move( (idealPos - p1View.getCenter()) * VIEWSPEED);

                        idealPos = viewPosition(p2.getPosition(), p1.getPosition());
                        p2View.move( (idealPos - p2View.getCenter()) * VIEWSPEED);
                }
                else
                {
                        //If we don't want a split view, then the ideal position is the halfway
                        //point between both players.
                        idealPos = (p1.getPosition() + p2.getPosition()) / 2.f;
                        p1View.move( (idealPos - p1View.getCenter()) * VIEWSPEED);
                        //Set player twos cameras to the same as player ones, this will avoid an jump if the cameras split again
                        //far away from where they last merged.
                        p2View.setCenter(p1View.getCenter());
                }

                //Draw everything from player ones point of view.
                window.clear();
                window.setView(p1View);
                window.draw(map);
                window.draw(p1);
                window.draw(p2);

                //If we're splitting the views then we'll do the same for player twos view.
                if(!singleView)
                {
                        //Draw all our normal stuff from players twos point of view.
                        p2Tex.clear();
                        p2Tex.setView(p2View);
                        p2Tex.draw(map);
                        p2Tex.draw(p1);
                        p2Tex.draw(p2);

                        //Now calculate where to place our rectangle to properly divide the screen.
                        //We start in the centre of the window.
                        eraser.setPosition(window.mapPixelToCoords(sf::Vector2i(WIDTH/2, HEIGHT/2), p2View));
                        //Then we calcuate a vector representing the line that will split the two views.
                        //This line is perpendicular to the line between both players.
                        sf::Vector2f angle = thor::perpendicularVector(p1.getPosition() - p2.getPosition());
                        thor::setLength(angle, static_cast<float>(HEIGHT));
                        //Move the rectangle along this line so that it will stretch across the entire screen.
                        eraser.move(angle);
                        //Rotate the rectangle so that it covers the side of the screen that we want to be blended with player ones view.
                        eraser.setRotation(-(thor::signedAngle(angle, sf::Vector2f(1, 1)) + 135));

                        //Draw the rectangle to our rendertexture using BlendMode None.
                        p2Tex.draw(eraser, sf::BlendMode::BlendNone);
                        p2Tex.display();

                        //Assign the tecture to a sprite.
                        p2Sprite.setTexture(p2Tex.getTexture());
                        //Draw it over the origin so that our rectangle math works out properly.
                        window.setView(window.getDefaultView());
                        //Draw it to the window.
                        window.draw(p2Sprite);
                }

                window.display();
        }


        return EXIT_SUCCESS;
}

bool shouldSplit(sf::Vector2f position1, sf::Vector2f position2)
{
        //If the two positions are more than a half of the average of the screens
        //width and height then we'll split.
        sf::Vector2f dist = position1 - position2;
        if(thor::length(dist) > (WIDTH + HEIGHT) / 4)
                return true;

        return false;
}

sf::Vector2f viewPosition(sf::Vector2f position1, sf::Vector2f position2)
{
        //Our camera position is currently set to keep the player within a circular area of the screen.
        //It would probably be better to convert this to instead keep them within a elliptical area.
        sf::Vector2f out = position1;

        //MAX_DISTANCE will keep position1 on the screen, reguardless of how far away it is from position2.
        const float MAX_DISTANCE = (WIDTH + HEIGHT) / 5;
        //this is the ideal position, the halfway point between both players, the camera will gravitate towards this position
        //so that things meet up nicely when the views merge.
        sf::Vector2f direction = (position2 - position1) / 2.f;

        //Use MAX_DISTANCE to trim our direction vector if it is too long,
        //eg. If it would put position1 off the edge of the screen.
        if(thor::length(direction) > MAX_DISTANCE)
                thor::setLength(direction, MAX_DISTANCE);

        return out + direction;
}

This should work as a good starting point for someone who wants to add this technique to their own project. Theirs are fair bit of cleaning up and improving to do, mostly the player positions should be constrained to an ellipsis rather than a circle, currently the players can end up far too close to the screen edge.

I'll be putting this up on my blog and the SFML wiki as as well, probably tomorrow or the day after.

Criticism welcome(especially when it comes to my maths), If anyone can think of any small features like this that they'd like to see in an example then tell me, I'll give it a shot.

Grimshaw

  • Hero Member
  • *****
  • Posts: 631
  • Nephilim SDK
    • View Profile
Re: Dynamic Split Screen
« Reply #1 on: March 12, 2014, 11:29:56 am »
Indeed looking good! Seems like a just fine implementation for split screen :)

eXpl0it3r

  • SFML Team
  • Hero Member
  • *****
  • Posts: 10823
    • View Profile
    • development blog
    • Email
Re: Dynamic Split Screen
« Reply #2 on: March 12, 2014, 11:40:23 am »
Very nice! :)
I've been thinking about implementing something like that as well, when I first saw the idea on a presentation of some indie game last year.
Official FAQ: https://www.sfml-dev.org/faq.php
Official Discord Server: https://discord.gg/nr4X7Fh
——————————————————————
Dev Blog: https://duerrenberger.dev/blog/

G.

  • Hero Member
  • *****
  • Posts: 1592
    • View Profile
Re: Dynamic Split Screen
« Reply #3 on: March 12, 2014, 01:53:03 pm »
I don't know if I'm retarded or if the heat wave got my brain, but it took me 15 min to figure out how your split screen works. I thought the "eraser" was only the separator so I didn't pay attention to it, until I said to myself that maybe "eraser" wasn't another word for "separator". :D It's pretty clever (IMO).
I used to do it with a fragment shader, displaying the 2nd player RenderTexture when the pixel was on the positive side of the separating line.

Gammenon

  • Newbie
  • *
  • Posts: 27
    • View Profile
Re: Dynamic Split Screen
« Reply #4 on: March 12, 2014, 02:17:28 pm »
Like Dragon ball Z games of 16-32 bit era ;)



Doodlemeat

  • Guest
Re: Dynamic Split Screen
« Reply #5 on: March 25, 2014, 08:38:45 am »
Cool! :)

When someone gave me a solution to try this by myself, I was worried that I had to draw everything out twice, but I think that would require one to have a bigger view to to draw stuff on. Nvm, this works far more better.

I was inspired by http://raptjs.com/