Move By Keyboard, Fire Towards Mouse#
This example uses a player-controlled tank to demonstrate the difference between the right and wrong way of rotating a turret around an attachment point. In both modes, the tank’s barrel follows the mouse. The turret sprite is ommitted to keep the rotation visible.
See the docstring, comments, and on screen instructions for further info.
1"""
2Sprite Rotation Around a Point, With A Tank
3
4Games often include elements that rotate toward targets. Common
5examples include gun turrets on vehicles and towers. In 2D games,
6these rotating parts are usually implemented as sprites that move
7relative to whatever they're attached to.
8
9There's a catch to this: you have to rotate these parts around their
10attachment points rather than the centers of their sprites. Otherwise,
11the rotation will look wrong!
12
13To illustrate the difference, this example uses a player-controllable
14tank with a barrel that follows the mouse. You can press P to switch
15between two ways of rotating the barrel:
161. Correctly, with the barrel's rear against the tank's center
172. Incorrectly, around the barrel's center pinned to the tank's
18
19Artwork from https://kenney.nl
20
21If Python and Arcade are installed, this example can be run from the command line with:
22python -m arcade.examples.sprite_rotate_around_tank
23"""
24from __future__ import annotations
25
26import math
27import arcade
28from arcade.types import Point
29from arcade.math import (
30 get_angle_radians,
31 rotate_point,
32 get_angle_degrees,
33)
34
35TANK_SPEED_PIXELS = 64 # How many pixels per second the tank travels
36TANK_TURN_SPEED_DEGREES = 70 # How fast the tank's body can turn
37
38
39# This is half the length of the barrel sprite.
40# We use it to ensure the barrel's rear sits in the middle of the tank
41TANK_BARREL_LENGTH_HALF = 15
42
43
44SCREEN_WIDTH = 800
45SCREEN_HEIGHT = 600
46SCREEN_MIDDLE = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
47
48
49SCREEN_TITLE = "Rotating Tank Example"
50
51
52# These paths are built-in resources included with arcade
53TANK_BODY = ":resources:images/topdown_tanks/tankBody_dark_outline.png"
54TANK_BARREL = ":resources:images/topdown_tanks/tankDark_barrel3_outline.png"
55
56
57class RotatingSprite(arcade.Sprite):
58 """
59 Sprite subclass which can be rotated around a point.
60
61 This version of the class always changes the angle of the sprite.
62 Other games might not rotate the sprite. For example, moving
63 platforms in a platformer wouldn't rotate.
64 """
65 def rotate_around_point(self, point: Point, degrees: float):
66 """
67 Rotate the sprite around a point by the set amount of degrees
68
69 :param point: The point that the sprite will rotate about
70 :param degrees: How many degrees to rotate the sprite
71 """
72
73 # Make the sprite turn as its position is moved
74 self.angle += degrees
75
76 # Move the sprite along a circle centered around the passed point
77 self.position = rotate_point(
78 self.center_x, self.center_y,
79 point[0], point[1], degrees)
80
81
82class ExampleWindow(arcade.Window):
83
84 def __init__(self):
85 super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
86
87 # Set Background to be green
88 self.background_color = arcade.csscolor.SEA_GREEN
89
90 # The tank and barrel sprite
91 self.tank = arcade.Sprite(TANK_BODY)
92 self.tank.position = SCREEN_MIDDLE
93
94 self.barrel = RotatingSprite(TANK_BARREL)
95 self.barrel.position =\
96 SCREEN_MIDDLE[0], SCREEN_MIDDLE[1] - TANK_BARREL_LENGTH_HALF
97
98 self.tank_direction = 0.0 # Forward & backward throttle
99 self.tank_turning = 0.0 # Turning strength to the left or right
100
101 self.mouse_pos = 0, 0
102
103 self.tank_sprite_list = arcade.SpriteList()
104 self.tank_sprite_list.extend([self.tank, self.barrel])
105
106 self._correct = True
107 self.correct_text = arcade.Text(
108 "Turret Rotation is Correct, Press P to Switch",
109 SCREEN_MIDDLE[0], SCREEN_HEIGHT - 25,
110 anchor_x='center')
111
112 self.control_text = arcade.Text(
113 "WASD to move tank, Mouse to aim",
114 SCREEN_MIDDLE[0], 15,
115 anchor_x='center')
116
117 def on_draw(self):
118 self.clear()
119 self.tank_sprite_list.draw()
120
121 self.control_text.draw()
122 self.correct_text.draw()
123
124 def on_update(self, delta_time: float):
125 self.move_tank(delta_time)
126
127 def move_tank(self, delta_time):
128 """
129 Perform all calculations for moving the tank's body and barrel
130 """
131
132 # Rotate the tank's body in place without changing position
133 # We'll rotate the barrel after updating the entire tank's x & y
134 self.tank.angle += TANK_TURN_SPEED_DEGREES\
135 * self.tank_turning * delta_time
136
137 # Calculate how much the tank should move forward or back
138 move_magnitude = self.tank_direction * TANK_SPEED_PIXELS * delta_time
139 x_dir = math.sin(self.tank.radians) * move_magnitude
140 y_dir = math.cos(self.tank.radians) * move_magnitude
141
142 # Move the tank's body
143 self.tank.position =\
144 self.tank.center_x + x_dir,\
145 self.tank.center_y + y_dir
146
147 # Move the barrel with the body
148 self.barrel.position =\
149 self.barrel.center_x + x_dir,\
150 self.barrel.center_y + y_dir
151
152 # Begin rotating the barrel by finding the angle to the mouse
153 mouse_angle = get_angle_degrees(
154 self.tank.center_x, self.tank.center_y,
155 self.mouse_pos[0], self.mouse_pos[1])
156
157 # Compensate for the flipped orientation of the barrel texture
158 # This could be skipped if the texture faced up instead
159 mouse_angle += 180
160
161 if self.correct:
162 # Rotate the barrel sprite with one end at the tank's center
163
164 # Subtract the old angle to get the change in angle
165 angle_change = mouse_angle - self.barrel.angle
166
167 self.barrel.rotate_around_point(self.tank.position, angle_change)
168 else:
169 # Swivel the barrel with its center aligned with the body's
170 self.barrel.angle = mouse_angle
171
172 def on_key_press(self, symbol: int, modifiers: int):
173 if symbol == arcade.key.W:
174 self.tank_direction += 1
175 elif symbol == arcade.key.S:
176 self.tank_direction -= 1
177 elif symbol == arcade.key.A:
178 self.tank_turning -= 1
179 elif symbol == arcade.key.D:
180 self.tank_turning += 1
181 elif symbol == arcade.key.P:
182 self.correct = not self.correct
183
184 self.correct_text.text =\
185 f"Turret Rotation is "\
186 f"{'Correct' if self.correct else 'Incorrect'},"\
187 f" Press P to Switch"
188
189 def on_key_release(self, symbol: int, modifiers: int):
190 if symbol == arcade.key.W:
191 self.tank_direction -= 1
192 elif symbol == arcade.key.S:
193 self.tank_direction += 1
194 elif symbol == arcade.key.A:
195 self.tank_turning += 1
196 elif symbol == arcade.key.D:
197 self.tank_turning -= 1
198
199 def on_mouse_motion(self, x: int, y: int, dx: int, dy: int):
200 self.mouse_pos = x, y
201
202 @property
203 def correct(self):
204 return self._correct
205
206 @correct.setter
207 def correct(self, correct: bool):
208 """
209 Move the tank's barrel between correct rotation and incorrect positions
210 """
211 self._correct = correct
212 if correct:
213 angle = get_angle_radians(
214 self.tank.center_y, self.tank.center_x,
215 self.mouse_pos[1], self.mouse_pos[0])
216
217 self.barrel.position = (
218 self.barrel.center_x + math.sin(angle) * TANK_BARREL_LENGTH_HALF,
219 self.barrel.center_y + math.cos(angle) * TANK_BARREL_LENGTH_HALF,
220 )
221
222 else:
223 self.barrel.position = self.tank.position
224
225
226def main():
227 window = ExampleWindow()
228 window.run()
229
230
231if __name__ == '__main__':
232 main()