Skip to content

Commit b4aa726

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 b4aa726

File tree

4 files changed

+350
-0
lines changed

4 files changed

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