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