Skip to content

Commit abbce1b

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 abbce1b

File tree

4 files changed

+345
-0
lines changed

4 files changed

+345
-0
lines changed
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
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()
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
61.6 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)