Skip to content

Commit 9c52854

Browse files
committed
Add a split screen example using Camera2D
This is a simple example of how to create a split screen example using Camera2D and uses the PyMunk physics engine.
1 parent 33e5960 commit 9c52854

File tree

4 files changed

+356
-0
lines changed

4 files changed

+356
-0
lines changed
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
"""
2+
A simple example that demonstrates using multiple cameras to allow a split
3+
screen using Arcade's 3.0 Camera2D.
4+
5+
The left screen follows the player that is controlled by WASD, and the right
6+
follows the player controlled by the keyboard.
7+
8+
If Python and Arcade are installed, this example can be run
9+
from the command line with:
10+
python -m arcade.examples.camera2d_splitscreen
11+
"""
12+
13+
from typing import List, Optional, Tuple
14+
15+
import pymunk
16+
17+
import arcade
18+
19+
20+
TITLE = "Split Screen Example"
21+
SCREEN_WIDTH = 1400
22+
SCREEN_HEIGHT = 1000
23+
BACKGROUND_COLOR = arcade.color.SPACE_CADET
24+
BACKGROUND_IMAGE = ":resources:images/backgrounds/stars.png"
25+
26+
DEFAULT_DAMPING = 1.0
27+
28+
GRAVITY = 0.0
29+
SHIP_MASS = 1.0
30+
SHIP_FRICTION = 0.0
31+
SHIP_ELASTICITY = 0.1
32+
33+
SHIP_FRICTION = 0.0
34+
ROTATION_SPEED = 0.05
35+
THRUSTER_FORCE = 200.0
36+
37+
SHIP_SCALING = 0.5
38+
39+
PLAYER_ONE = 0
40+
PLAYER_TWO = 1
41+
42+
CAMERA_ONE = 0
43+
CAMERA_TWO = 1
44+
45+
46+
class 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+
141+
class 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.equalise()
232+
player_two_camera.equalise()
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].equalise()
314+
self.cameras[PLAYER_TWO].equalise()
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+
324+
if __name__ == "__main__":
325+
window = Game()
326+
window.setup()
327+
arcade.run()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
:orphan:
2+
3+
.. _camera2d_splitscreen:
4+
5+
Two Player Split Screen
6+
=======================================
7+
8+
A game can create a split screen for each player using two :class:`arcade.Camera2D`
9+
and each camera's viewport.
10+
11+
After we call :function:`arcade.Camera2D.use` on each :class:`arcade.Camera2D` instance
12+
we then draw the sprites we want to render for that camera.
13+
14+
See also :ref:`sprite_move_scrolling_box`.
15+
16+
.. image:: images/camera2d_splitscreen.png
17+
:width: 600px
18+
:align: center
19+
:alt: Screen shot of using split screens
20+
21+
.. literalinclude:: ../../arcade/examples/camera2d_splitscreen.py
22+
:caption: camera2d_splitscreen.py
23+
:linenos:
24+
:emphasize-lines: 145-149, 206-228, 251-253, 256-260, 263-292
256 KB
Loading

doc/example_code/index.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,11 @@ Cameras
449449

450450
:ref:`camera_platform`
451451

452+
.. figure:: images/thumbs/camera2d_splitscreen.png
453+
:figwidth: 170px
454+
:target: camera2d_splitscreen.html
455+
456+
:ref:`camera2d_splitscreen`
452457

453458
.. _view_examples:
454459

0 commit comments

Comments
 (0)