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