Skip to content

Commit ec6c1e6

Browse files
DragonMoffonpushfoo
authored andcommitted
New Isometric Example
A new Isometric example which uses and OthrographicProjector and a BillboardList to show how you could implement an isometric view in a game.
1 parent f373c74 commit ec6c1e6

File tree

1 file changed

+167
-0
lines changed

1 file changed

+167
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# flake8: noqa
2+
"""
3+
Using the Orthographic Projector to make a cool isometric scene.
4+
5+
By using a SpriteList and a BillboardList we can create the floor and a little character
6+
to move around the scene.
7+
8+
The traditional isometric perspective comes from two effects. Firstly the camera rotates "upwards" by 30 degrees.
9+
Then it is rotated by 45 degrees. Together with an orthographic camera allows you
10+
to show all 3 dimensions, but depth and vertical height line up so you do loose some dimensionality.
11+
12+
Mathematically this is done using cos and sin of 45 and 60 degrees. We use 60 because it is 90 - 30
13+
to get the "upwards" motion. These values can be represented by fractions so we can avoid using
14+
the actual acos and asin methods.
15+
16+
first the 30 degree rotation forward vector
17+
(0.0, 0.0, -1.0) -> (0.0, sin(60), -cos(60)) = (0.0, sqrt(3)/2, -0.5)
18+
second the 45 degree rotation around the y-axis
19+
(0.0, sqrt(3)/2, -0.5) -> (cos(45) * sqrt(3)/2, sin(45) * sqrt(3) / 2.0, -0.5)
20+
21+
this makes the final forward vector
22+
(sqrt(2) * sqrt(3) / 4, sqrt(2) * sqrt(3) / 4, -0.5) = ((3.0 / 8.0)**0.5, (3.0 / 8.0)**0.5, -0.5)
23+
24+
The up vector is similarly calculated.
25+
26+
If Python and Arcade are installed, this example can be run from the command line with:
27+
python -m arcade.examples.isometric_with_billboards
28+
"""
29+
30+
import arcade
31+
32+
33+
PLAYER_WIDTH, PLAYER_HEIGHT = 32, 64
34+
35+
MAP_WIDTH, MAP_HEIGHT = 32, 32
36+
TILE_SIZE = 32
37+
38+
PLAYER_JUMP_SPEED = 300.0
39+
PLAYER_MOVE_SPEED = 200.0
40+
GRAVITY = -800.0
41+
42+
43+
class Isometric(arcade.Window):
44+
45+
def __init__(self):
46+
super().__init__(800, 600, "Isometric")
47+
48+
# The math required to accurately find the up vector is complex so we just let arcade do it.
49+
# Though one difference is that we make the up vector sit along the +z axis. This makes sure that
50+
# the final up vector calculated by arcade is also roughly along the +z axis.
51+
self.camera_data = arcade.camera.CameraData(
52+
(0.0, 0.0, 0.0), # Position
53+
(0.0, 0.0, 1.0), # Up
54+
((3.0 / 8.0)**0.5, (3.0 / 8.0)**0.5, -0.5), # Forward
55+
1.0 # Zoom
56+
)
57+
arcade.camera.data_types.constrain_camera_data(self.camera_data, True)
58+
self.isometric_camera = arcade.camera.OrthographicProjector(view=self.camera_data)
59+
60+
# Due to the fact that the sprites are now moving into and away from the screen we need to expand
61+
# the near and far planes as the sprites will get cut off if we don't
62+
self.isometric_camera.projection.near = -600
63+
self.isometric_camera.projection.far = 600
64+
65+
self.player_sprite = arcade.SpriteSolidColor(PLAYER_WIDTH, PLAYER_HEIGHT, color=arcade.color.RADICAL_RED)
66+
67+
self.player_vertical_velocity = 0.0
68+
69+
# If the player is
70+
self.player_on_ground = True
71+
72+
# Since the isometric perspective rotates the x and y axis we instead care about
73+
# Moving the player up/down or left/right along the screen.
74+
self.move_player_ns = 0
75+
self.move_player_ew = 0
76+
77+
# By giving the player a shadow it becomes easier to place them in the world.
78+
# This is a very common trick used by 3D platformers.
79+
self.player_shadow = arcade.SpriteCircle(PLAYER_WIDTH, color=(0, 0, 0, 124))
80+
81+
self.billboard_list = arcade.sprite_list.BillboardList()
82+
self.billboard_list.append(self.player_sprite)
83+
84+
self.sprite_list = arcade.sprite_list.SpriteList()
85+
86+
for x_idx in range(-MAP_WIDTH//2, MAP_WIDTH//2):
87+
for y_idx in range(-MAP_HEIGHT//2, MAP_HEIGHT//2):
88+
# We add one to the tile size so there is a 1 pixel gap between every tile.
89+
#
90+
x = x_idx * (TILE_SIZE + 1)
91+
y = y_idx * (TILE_SIZE + 1)
92+
tile = arcade.SpriteSolidColor(TILE_SIZE, TILE_SIZE, x, y, arcade.color.WHITE)
93+
self.sprite_list.append(tile)
94+
self.sprite_list.append(self.player_shadow)
95+
96+
def on_update(self, delta_time: float):
97+
self.player_vertical_velocity += GRAVITY * delta_time
98+
self.player_sprite.depth += self.player_vertical_velocity * delta_time
99+
if self.player_sprite.depth <= 0.0:
100+
self.player_sprite.depth = 0.0
101+
self.player_vertical_velocity = 0.0
102+
self.player_on_ground = True
103+
else:
104+
self.player_on_ground = False
105+
106+
# Because the camera is rotated 45 moving the player's y value doesn't actually represent going up and down
107+
# the screen. Instead, moving upward on the screen requires increasing both the x and y values.
108+
# Moving left or right requires increase on axis and decreasing the other.
109+
move_player_x = self.move_player_ns + self.move_player_ew
110+
move_player_y = self.move_player_ns - self.move_player_ew
111+
112+
# We get the move speed so we can scale the final x and y speed. If we didn't it would be faster
113+
# To move diagonally that horizontally or vertically.
114+
move_player_speed = (move_player_x**2 + move_player_y**2)**0.5
115+
116+
if move_player_speed == 0.0:
117+
move_player_speed = 1.0
118+
119+
player_x_speed = move_player_x / move_player_speed * PLAYER_MOVE_SPEED * delta_time
120+
player_y_speed = move_player_y / move_player_speed * PLAYER_MOVE_SPEED * delta_time
121+
122+
new_player_position = self.player_sprite.center_x + player_x_speed, self.player_sprite.center_y + player_y_speed
123+
124+
# Because we are working with the center of the player sprite we need to shift the shadow position downwards
125+
# Normally we would use the bottom value of the player sprite, but because the player is being
126+
# Billboarded that value is not what is seen on screen. 0.6 was chosen because it looks right.
127+
new_shadow_position = new_player_position[0] - PLAYER_HEIGHT*0.6, new_player_position[1] - PLAYER_HEIGHT*0.6
128+
129+
self.player_sprite.position = new_player_position
130+
self.player_shadow.position = new_shadow_position
131+
132+
def on_key_press(self, symbol: int, modifiers: int):
133+
if symbol == arcade.key.SPACE and self.player_on_ground:
134+
self.player_vertical_velocity += PLAYER_JUMP_SPEED
135+
elif symbol == arcade.key.W:
136+
self.move_player_ns += 1
137+
elif symbol == arcade.key.S:
138+
self.move_player_ns += -1
139+
elif symbol == arcade.key.D:
140+
self.move_player_ew += 1
141+
elif symbol == arcade.key.A:
142+
self.move_player_ew += -1
143+
144+
def on_key_release(self, symbol: int, modifiers: int):
145+
if symbol == arcade.key.W:
146+
self.move_player_ns += -1
147+
elif symbol == arcade.key.S:
148+
self.move_player_ns += 1
149+
elif symbol == arcade.key.D:
150+
self.move_player_ew += -1
151+
elif symbol == arcade.key.A:
152+
self.move_player_ew += 1
153+
154+
def on_draw(self):
155+
self.clear()
156+
with self.isometric_camera.activate():
157+
self.sprite_list.draw()
158+
self.billboard_list.draw()
159+
160+
161+
def main():
162+
window = Isometric()
163+
window.run()
164+
165+
166+
if __name__ == '__main__':
167+
main()

0 commit comments

Comments
 (0)