pythonarcade

Arcade Split Screen Implementation


I'm making a top-down racing game that has local multiplayer. I want each player to have their own view on the screen. The problem I'm having is that it seems you can't change a camera's position within the window. Ideally, I want to be able to have 4 different views of that map (1 for each player). If anyone knows of a way to change the camera's position within the window or a better way to do this, it would be much appreciated.

Here is an example I made which has 2 cameras, one is as big as the window, the other is smaller. I want them to both take up an equal portion of the screen and not overlap each other.

import arcade as arc
from arcade.pymunk_physics_engine import PymunkPhysicsEngine

SCREEN_WIDTH = 700
SCREEN_HEIGHT = 500

LEFT_VIEWPORT_MARGIN = 100
RIGHT_VIEWPORT_MARGIN = 100
TOP_VIEWPORT_MARGIN = 100
BOTTOM_VIEWPORT_MARGIN = 100


PHY_ENGINE = PymunkPhysicsEngine()


class Player(arc.Sprite):
    def __init__(self, player_num=0, control="wasd"):
        super().__init__()
        if player_num == 0:
            self.texture = arc.load_texture(":resources:images/space_shooter/playerShip1_orange.png")
        elif player_num == 1:
            self.texture = arc.load_texture(":resources:images/space_shooter/playerShip1_blue.png")

        self.control = control

        self.move_up = False
        self.move_down = False
        self.move_left = False
        self.move_right = False

    def update(self):
        if self.move_up:
            PHY_ENGINE.apply_force(self, (0, 1000))
        if self.move_down:
            PHY_ENGINE.apply_force(self, (0, -1000))
        if self.move_right:
            PHY_ENGINE.apply_force(self, (1000, 0))
        if self.move_left:
            PHY_ENGINE.apply_force(self, (-1000, 0))


class GameView(arc.View):
    def __init__(self):
        super().__init__()
        self.width = SCREEN_WIDTH
        self.height = SCREEN_HEIGHT

        self.scene = None

        self.p_cameras = []
        self.gui_camera = None
        self.camera_info = [[0, 0], [0, 0]]
        self.map_width = 10000
        self.map_height = 10000

        self.players = []

        # input stuff
        self.w_pressed = False
        self.s_pressed = False
        self.a_pressed = False
        self.d_pressed = False

        self.up_pressed = False
        self.down_pressed = False
        self.left_pressed = False
        self.right_pressed = False

        self.move_up = False
        self.move_down = False
        self.move_left = False
        self.move_right = False

        # game stuff
        self.physics_engine = PHY_ENGINE
        self.background = arc.load_texture(":resources:images/backgrounds/abstract_1.jpg")

    def process_keychange(self):
        for player in self.players:
            # WASD movement
            if player.control == "wasd":
                if self.w_pressed:
                    player.move_up = True
                else:
                    player.move_up = False

                if self.s_pressed:
                    player.move_down = True
                else:
                    player.move_down = False

                if self.d_pressed:
                    player.move_right = True
                else:
                    player.move_right = False

                if self.a_pressed:
                    player.move_left = True
                else:
                    player.move_left = False

            # arrow key movement
            elif player.control == "arrows":
                if self.up_pressed:
                    player.move_up = True
                else:
                    player.move_up = False

                if self.down_pressed:
                    player.move_down = True
                else:
                    player.move_down = False

                if self.right_pressed:
                    player.move_right = True
                else:
                    player.move_right = False

                if self.left_pressed:
                    player.move_left = True
                else:
                    player.move_left = False

    def on_key_press(self, key, modifiers):
        if key == arc.key.W:
            self.w_pressed = True
        if key == arc.key.S:
            self.s_pressed = True
        if key == arc.key.A:
            self.a_pressed = True
        if key == arc.key.D:
            self.d_pressed = True

        if key == arc.key.UP:
            self.up_pressed = True
        if key == arc.key.DOWN:
            self.down_pressed = True
        if key == arc.key.LEFT:
            self.left_pressed = True
        if key == arc.key.RIGHT:
            self.right_pressed = True

        if key == arc.key.ESCAPE:
            arc.exit()

    def on_key_release(self, key, modifiers):
        if key == arc.key.W:
            self.w_pressed = False
        if key == arc.key.S:
            self.s_pressed = False
        if key == arc.key.A:
            self.a_pressed = False
        if key == arc.key.D:
            self.d_pressed = False

        if key == arc.key.UP:
            self.up_pressed = False
        if key == arc.key.DOWN:
            self.down_pressed = False
        if key == arc.key.LEFT:
            self.left_pressed = False
        if key == arc.key.RIGHT:
            self.right_pressed = False

        self.process_keychange()

    def on_show_view(self):
        arc.set_viewport(0, self.window.width, 0, self.window.height)

        self.load_level()

    def load_level(self):
        self.p_cameras = [arc.Camera(), arc.Camera(viewport_width=500, viewport_height=400)]
        self.scene = arc.Scene()
        self.scene.add_sprite_list("player")

        player1 = Player()
        player1.center_y = 200
        player1.center_x = 200
        self.scene.add_sprite("player", player1)
        self.physics_engine.add_sprite(player1, moment_of_inertia=PymunkPhysicsEngine.MOMENT_INF,
                                       collision_type="player", damping=.01)

        player2 = Player(player_num=1, control="arrows")
        player2.center_y = 300
        player2.center_x = 200
        self.scene.add_sprite("player", player2)
        self.physics_engine.add_sprite(player2, moment_of_inertia=PymunkPhysicsEngine.MOMENT_INF,
                                       collision_type="player", damping=.01)

        self.players.append(player1)
        self.players.append(player2)

    def on_draw(self):
        if self.p_cameras is None:
            return

        for camera in self.p_cameras:
            camera.use()
            arc.draw_lrwh_rectangle_textured(camera.position.x,
                                             camera.position.y,
                                             camera.viewport_width, camera.viewport_height, self.background)
            self.scene.draw()

    def center_camera_to_player(self, index=0):
        target_player = self.players[index]
        camera = self.p_cameras[index]
        view_left = self.camera_info[index][0]
        view_bottom = self.camera_info[index][1]
        # Scroll left
        left_boundary = view_left + LEFT_VIEWPORT_MARGIN
        if target_player.left < left_boundary:
            view_left -= left_boundary - target_player.left

        # Scroll right
        right_boundary = view_left + self.width - RIGHT_VIEWPORT_MARGIN - (SCREEN_WIDTH - camera.viewport_width)
        if target_player.right > right_boundary:
            view_left += target_player.right - right_boundary

        # Scroll up
        top_boundary = view_bottom + self.height - TOP_VIEWPORT_MARGIN - (SCREEN_HEIGHT - camera.viewport_height)
        if target_player.top > top_boundary:
            view_bottom += target_player.top - top_boundary

        # Scroll down
        bottom_boundary = view_bottom + BOTTOM_VIEWPORT_MARGIN
        if target_player.bottom < bottom_boundary:
            view_bottom -= bottom_boundary - target_player.bottom

        # keeps camera in left bound of map
        if view_left < 0:
            view_left = 0

        # keeps camera in right bound of map
        if (view_left + self.width) > self.map_width:
            view_left = self.map_width - self.width

        # keeps camera in bottom bound of map
        if view_bottom < 0:
            view_bottom = 0

        # keeps camera in top bound of map
        if view_bottom + self.height > self.map_height:
            view_bottom = self.map_height - self.height

        # Scroll to the proper location
        position = view_left, view_bottom
        camera.move_to(position, .3)

    def on_update(self, delta_time: float):
        self.process_keychange()
        self.center_camera_to_player(0)
        self.center_camera_to_player(1)
        self.scene.update()
        self.physics_engine.step()


def main():
    """Main function"""
    window = arc.Window(SCREEN_WIDTH, SCREEN_HEIGHT)
    start_view = GameView()
    window.show_view(start_view)
    arc.run()


if __name__ == "__main__":
    main()


Solution

  • The cameras in 2.6 doesn't support split screen (they will in 3.0+). However, it's not hard to set viewport and projection to achieve this.

    Assumming you have a 200 x 100 window you can define two smaller screen areas by setting the viewport manually. Then the projection decides what range of geometry will be rendered inside those viewports.

    Simple example

    window.ctx.viewport = 0, 0, 100, 100  # (XYWH) A 100x100 area at 0,0
    window.ctx.projection_2d = 0, 100, 0, 100
    # Draw left part here
    
    
    window.ctx.viewport = 100, 0, 100, 100  # (XYWH) A 100x100 area at 100,0
    window.ctx.projection_2d = 0, 100, 0, 100
    # Draw right part here
    

    Usually you want the projection to have the same size as the viewport to get 1:1 pixel ratio. Everything in arcade is basically trinagles, so your defining what range of your "world" should be projected/rendered into the area. These are lower level OpenGL functions, so all this tranformation will be harware accellerated.

    It becomes more obvious how it works when you play with the values.