|
| 1 | +from typing import List, Optional, Tuple |
| 2 | + |
| 3 | +import arcade |
| 4 | + |
| 5 | +""" A simple example that demonstrates using multiple cameras to allow a split |
| 6 | +screen using Arcade's 3.0 Camera2D. |
| 7 | +
|
| 8 | +The left screen follows the player that is controlled by WASD, and the right |
| 9 | +follows the player controlled by the keyboard. |
| 10 | +""" |
| 11 | + |
| 12 | +TITLE = "Split Screen Example" |
| 13 | +SCREEN_WIDTH = 1400 |
| 14 | +SCREEN_HEIGHT = 1000 |
| 15 | +BACKGROUND_COLOR = arcade.color.SPACE_CADET |
| 16 | +BACKGROUND_IMAGE = ":resources:images/backgrounds/stars.png" |
| 17 | + |
| 18 | +DEFAULT_DAMPING = 1.0 |
| 19 | + |
| 20 | +GRAVITY = 0.0 |
| 21 | +SHIP_MASS = 1.0 |
| 22 | +SHIP_FRICTION = 0.0 |
| 23 | +SHIP_ELASTICITY = 0.1 |
| 24 | + |
| 25 | +SHIP_FRICTION = 0.0 |
| 26 | +ROTATION_SPEED = 0.05 |
| 27 | +THRUSTER_FORCE = 200.0 |
| 28 | + |
| 29 | +SHIP_SCALING = 0.5 |
| 30 | + |
| 31 | +PLAYER_ONE = 0 |
| 32 | +PLAYER_TWO = 1 |
| 33 | + |
| 34 | +CAMERA_ONE = 0 |
| 35 | +CAMERA_TWO = 1 |
| 36 | + |
| 37 | + |
| 38 | +class Player(arcade.Sprite): |
| 39 | + def __init__(self, main, |
| 40 | + start_position: Tuple, |
| 41 | + player_num: int): |
| 42 | + self.shape = None |
| 43 | + self.sprite_filename = ":resources:images/space_shooter/playerShip1_orange.png" |
| 44 | + self.player_num = player_num |
| 45 | + self.dx = 0.0 |
| 46 | + self.dy = 0.0 |
| 47 | + self.force = 0.0 |
| 48 | + self.applied_rotational_vel = 0 |
| 49 | + self.body = None |
| 50 | + self.start_position = start_position |
| 51 | + self.friction = SHIP_FRICTION |
| 52 | + |
| 53 | + self.w_pressed = 0.0 |
| 54 | + self.s_pressed = 0.0 |
| 55 | + self.a_pressed = 0.0 |
| 56 | + self.d_pressed = 0.0 |
| 57 | + |
| 58 | + self.left_pressed = 0.0 |
| 59 | + self.right_pressed = 0.0 |
| 60 | + self.up_pressed = 0.0 |
| 61 | + self.down_pressed = 0.0 |
| 62 | + |
| 63 | + super().__init__(self.sprite_filename) |
| 64 | + self.position = start_position |
| 65 | + self.mass = SHIP_MASS |
| 66 | + self.friction = SHIP_FRICTION |
| 67 | + self.elasticity = SHIP_ELASTICITY |
| 68 | + self.texture = arcade.load_texture(self.sprite_filename, |
| 69 | + hit_box_algorithm=arcade.hitbox.PymunkHitBoxAlgorithm()) |
| 70 | + self.main = main |
| 71 | + self.scale = SHIP_SCALING |
| 72 | + |
| 73 | + def setup(self): |
| 74 | + self.body = self.main.physics_engine.get_physics_object(self).body |
| 75 | + self.shape = self.main.physics_engine.get_physics_object(self).shape |
| 76 | + |
| 77 | + def apply_angle_damping(self): |
| 78 | + self.body.angular_velocity /= 1.05 |
| 79 | + |
| 80 | + def update(self, delta_time: float): |
| 81 | + super().update(delta_time) |
| 82 | + |
| 83 | + if self.player_num == PLAYER_ONE: |
| 84 | + self.dx = self.a_pressed + self.d_pressed |
| 85 | + self.dy = self.w_pressed + self.s_pressed |
| 86 | + |
| 87 | + elif self.player_num == PLAYER_TWO: |
| 88 | + self.dx = self.right_pressed + self.left_pressed |
| 89 | + self.dy = self.up_pressed + self.down_pressed |
| 90 | + |
| 91 | + self.body.apply_force_at_world_point((self.dx, -self.dy), (self.center_x, self.center_y)) |
| 92 | + |
| 93 | + def on_key_press(self, key: int, modifiers: int): |
| 94 | + if key == arcade.key.W: |
| 95 | + self.w_pressed = -THRUSTER_FORCE |
| 96 | + elif key == arcade.key.S: |
| 97 | + self.s_pressed = THRUSTER_FORCE |
| 98 | + elif key == arcade.key.A: |
| 99 | + self.a_pressed = -THRUSTER_FORCE |
| 100 | + elif key == arcade.key.D: |
| 101 | + self.d_pressed = THRUSTER_FORCE |
| 102 | + elif key == arcade.key.LEFT: |
| 103 | + self.left_pressed = -THRUSTER_FORCE |
| 104 | + elif key == arcade.key.RIGHT: |
| 105 | + self.right_pressed = THRUSTER_FORCE |
| 106 | + elif key == arcade.key.UP: |
| 107 | + self.up_pressed = -THRUSTER_FORCE |
| 108 | + elif key == arcade.key.DOWN: |
| 109 | + self.down_pressed = THRUSTER_FORCE |
| 110 | + |
| 111 | + def on_key_release(self, key: int, modifiers: int): |
| 112 | + if key == arcade.key.W: |
| 113 | + self.w_pressed = 0.0 |
| 114 | + elif key == arcade.key.S: |
| 115 | + self.s_pressed = 0.0 |
| 116 | + elif key == arcade.key.A: |
| 117 | + self.a_pressed = 0.0 |
| 118 | + elif key == arcade.key.D: |
| 119 | + self.d_pressed = 0.0 |
| 120 | + elif key == arcade.key.LEFT: |
| 121 | + self.left_pressed = 0.0 |
| 122 | + elif key == arcade.key.RIGHT: |
| 123 | + self.right_pressed = 0.0 |
| 124 | + elif key == arcade.key.UP: |
| 125 | + self.up_pressed = 0.0 |
| 126 | + elif key == arcade.key.DOWN: |
| 127 | + self.down_pressed = 0.0 |
| 128 | + |
| 129 | + |
| 130 | +class Game(arcade.Window): |
| 131 | + def __init__(self): |
| 132 | + |
| 133 | + self.screen_width: int = SCREEN_WIDTH |
| 134 | + self.screen_height: int = SCREEN_HEIGHT |
| 135 | + |
| 136 | + super().__init__(self.screen_width, |
| 137 | + self.screen_height, |
| 138 | + TITLE, |
| 139 | + resizable=True) |
| 140 | + arcade.set_background_color(BACKGROUND_COLOR) |
| 141 | + |
| 142 | + self.background_image:str = BACKGROUND_IMAGE |
| 143 | + self.physics_engine: Optional[arcade.PymunkPhysicsEngine] = None |
| 144 | + |
| 145 | + self.players: Optional[arcade.SpriteList] = None |
| 146 | + self.players_list = [] |
| 147 | + |
| 148 | + self.cameras: List[arcade.Camera2D] = [] |
| 149 | + self.divider: Optional[arcade.SpriteList] = None |
| 150 | + |
| 151 | + def setup(self): |
| 152 | + self.setup_spritelists() |
| 153 | + self.setup_physics_engine() |
| 154 | + self.setup_players() |
| 155 | + self.setup_players_cameras() |
| 156 | + self.setup_divider() |
| 157 | + self.background = arcade.load_texture(self.background_image) |
| 158 | + |
| 159 | + def setup_divider(self): |
| 160 | + # It is helpful to have a divider, else the area between |
| 161 | + # the two splits can be hard to see. |
| 162 | + self.divider = arcade.SpriteList() |
| 163 | + self.divider_sprite = arcade.sprite.SpriteSolidColor( |
| 164 | + center_x = self.screen_width / 2, |
| 165 | + center_y = self.screen_height / 2, |
| 166 | + width=3, |
| 167 | + height=self.screen_height, |
| 168 | + color=arcade.color.WHITE |
| 169 | + ) |
| 170 | + self.divider.append(self.divider_sprite) |
| 171 | + |
| 172 | + def setup_spritelists(self): |
| 173 | + self.players = arcade.SpriteList() |
| 174 | + |
| 175 | + def setup_physics_engine(self): |
| 176 | + self.physics_engine = arcade.PymunkPhysicsEngine(damping=DEFAULT_DAMPING, |
| 177 | + gravity=(0, 0)) |
| 178 | + |
| 179 | + def setup_players(self): |
| 180 | + self.players.append(Player(self, |
| 181 | + (100, 100), |
| 182 | + PLAYER_ONE)) |
| 183 | + self.players.append(Player(self, |
| 184 | + (150, 150), |
| 185 | + PLAYER_TWO)) |
| 186 | + |
| 187 | + self.players_list = [self.players[PLAYER_ONE], self.players[PLAYER_TWO]] |
| 188 | + |
| 189 | + self.physics_engine.add_sprite(self.players[PLAYER_ONE], |
| 190 | + friction=self.players[PLAYER_ONE].friction, |
| 191 | + elasticity=self.players[PLAYER_ONE].elasticity, |
| 192 | + mass=self.players[PLAYER_ONE].mass, |
| 193 | + moment_of_inertia=arcade.PymunkPhysicsEngine.MOMENT_INF, |
| 194 | + collision_type="SHIP") |
| 195 | + |
| 196 | + self.physics_engine.add_sprite(self.players[PLAYER_TWO], |
| 197 | + friction=self.players[PLAYER_TWO].friction, |
| 198 | + elasticity=self.players[PLAYER_TWO].elasticity, |
| 199 | + mass=self.players[PLAYER_TWO].mass, |
| 200 | + moment_of_inertia=arcade.PymunkPhysicsEngine.MOMENT_INF, |
| 201 | + collision_type="SHIP") |
| 202 | + |
| 203 | + for player in self.players: |
| 204 | + player.setup() |
| 205 | + |
| 206 | + def setup_players_cameras(self): |
| 207 | + half_width = self.screen_width // 2 |
| 208 | + |
| 209 | + # We will make two cameras for each of our players. |
| 210 | + player_one_camera = arcade.camera.Camera2D() |
| 211 | + player_two_camera = arcade.camera.Camera2D() |
| 212 | + |
| 213 | + # We can adjust each camera's viewport to create our split screens |
| 214 | + player_one_camera.viewport = arcade.LBWH(0, 0, half_width, self.screen_height) |
| 215 | + player_two_camera.viewport = arcade.LBWH(half_width, 0, half_width, self.screen_height) |
| 216 | + |
| 217 | + # Calling equalise will equalise/equalize the Camera's projection |
| 218 | + # to match the viewport. If we don't call equalise, proportions |
| 219 | + # of our sprites can appear off. |
| 220 | + player_one_camera.equalise() |
| 221 | + player_two_camera.equalise() |
| 222 | + |
| 223 | + # Save a list of our cameras for later use |
| 224 | + self.cameras.append(player_one_camera) |
| 225 | + self.cameras.append(player_two_camera) |
| 226 | + |
| 227 | + self.center_camera_on_player(PLAYER_ONE) |
| 228 | + self.center_camera_on_player(PLAYER_TWO) |
| 229 | + |
| 230 | + def on_key_press(self, key: int, modifiers: int): |
| 231 | + for player in self.players: |
| 232 | + player.on_key_press(key, modifiers) |
| 233 | + |
| 234 | + if key == arcade.key.MINUS: |
| 235 | + self.zoom_cameras_out() |
| 236 | + elif key == arcade.key.EQUAL: |
| 237 | + self.zoom_cameras_in() |
| 238 | + |
| 239 | + def on_key_release(self, key: int, modifers: int): |
| 240 | + for player in self.players: |
| 241 | + player.on_key_release(key, modifers) |
| 242 | + |
| 243 | + def zoom_cameras_out(self): |
| 244 | + for camera in self.cameras: |
| 245 | + camera.zoom -= 0.1 |
| 246 | + |
| 247 | + def zoom_cameras_in(self): |
| 248 | + for camera in self.cameras: |
| 249 | + camera.zoom += 0.1 |
| 250 | + |
| 251 | + def center_camera_on_player(self, player_num): |
| 252 | + self.cameras[player_num].position = (self.players_list[player_num].center_x, |
| 253 | + self.players_list[player_num].center_y) |
| 254 | + |
| 255 | + def on_update(self, delta_time: float): |
| 256 | + self.players.update(delta_time) |
| 257 | + self.physics_engine.step() |
| 258 | + for player in range(len(self.players_list)): |
| 259 | + # After the player moves, center the camera on the player. |
| 260 | + self.center_camera_on_player(player) |
| 261 | + |
| 262 | + def on_draw(self): |
| 263 | + # Loop through our cameras, and then draw our objects. |
| 264 | + # |
| 265 | + # If an object should be drawn on both splits, we will |
| 266 | + # need to draw it for each camera, thus the draw functions |
| 267 | + # will be called twice (because of our loop). |
| 268 | + # |
| 269 | + # However, if desired, we could draw elements specific to |
| 270 | + # each camera, like a player HUD. |
| 271 | + for camera in range(len(self.cameras)): |
| 272 | + # Activate each players camera, clear it, then draw |
| 273 | + # the things we want to display on it. |
| 274 | + self.cameras[camera].use() |
| 275 | + self.clear() |
| 276 | + |
| 277 | + # We want both players to appear in each splitscreen, |
| 278 | + # so draw them for each camera. |
| 279 | + self.players.draw() |
| 280 | + |
| 281 | + # Likewise, we want the background to appear on |
| 282 | + # both splitscreens. |
| 283 | + arcade.draw_texture_rect( |
| 284 | + self.background, |
| 285 | + arcade.LBWH(0, 0, self.screen_width, self.screen_height) |
| 286 | + ) |
| 287 | + |
| 288 | + # The default_camera is a property of arcade.Window and we |
| 289 | + # can use it do draw our divider, or other shared elements, |
| 290 | + # such as a score, or other GUIs. |
| 291 | + self.default_camera.use() |
| 292 | + self.divider.draw() |
| 293 | + |
| 294 | + def on_resize(self, width: float, height: float): |
| 295 | + # We can easily resize the window with split screens by adjusting |
| 296 | + # the viewport in a similar manner to how we created them. Just |
| 297 | + # remember to call equalise! |
| 298 | + half_width = width // 2 |
| 299 | + |
| 300 | + self.cameras[PLAYER_ONE].viewport = arcade.LBWH(0, 0, half_width, height) |
| 301 | + self.cameras[PLAYER_TWO].viewport = arcade.LBWH(half_width, 0, half_width, height) |
| 302 | + self.cameras[PLAYER_ONE].equalise() |
| 303 | + self.cameras[PLAYER_TWO].equalise() |
| 304 | + |
| 305 | + # Our divider sprite location will need to be adjusted as |
| 306 | + # we used the screen's width and height to set it's location |
| 307 | + # earlier |
| 308 | + self.divider_sprite.height = height |
| 309 | + self.divider_sprite.center_x = width / 2 |
| 310 | + self.divider_sprite.center_y = height / 2 |
| 311 | + |
| 312 | + |
| 313 | +if __name__ == "__main__": |
| 314 | + window = Game() |
| 315 | + window.setup() |
| 316 | + arcade.run() |
0 commit comments