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.
sprite_rotation_around_tank.py
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"""
24import math
25import arcade
26from arcade.types import Point
27from arcade.math import (
28 get_angle_radians,
29 rotate_point,
30 get_angle_degrees,
31)
32
33TANK_SPEED_PIXELS = 64 # How many pixels per second the tank travels
34TANK_TURN_SPEED_DEGREES = 70 # How fast the tank's body can turn
35
36
37# This is half the length of the barrel sprite.
38# We use it to ensure the barrel's rear sits in the middle of the tank
39TANK_BARREL_LENGTH_HALF = 15
40
41
42WINDOW_WIDTH = 1280
43WINDOW_HEIGHT = 720
44WINDOW_TITLE = "Rotating Tank Example"
45
46
47# These paths are built-in resources included with arcade
48TANK_BODY = ":resources:images/topdown_tanks/tankBody_dark_outline.png"
49TANK_BARREL = ":resources:images/topdown_tanks/tankDark_barrel3_outline.png"
50
51
52class RotatingSprite(arcade.Sprite):
53 """
54 Sprite subclass which can be rotated around a point.
55
56 This version of the class always changes the angle of the sprite.
57 Other games might not rotate the sprite. For example, moving
58 platforms in a platformer wouldn't rotate.
59 """
60 def rotate_around_point(self, point: Point, degrees: float):
61 """
62 Rotate the sprite around a point by the set amount of degrees
63
64 Args:
65 point: The point that the sprite will rotate about
66 degrees: How many degrees to rotate the sprite
67 """
68
69 # Make the sprite turn as its position is moved
70 self.angle += degrees
71
72 # Move the sprite along a circle centered around the passed point
73 self.position = rotate_point(
74 self.center_x, self.center_y,
75 point[0], point[1], degrees)
76
77 def face_point(self, point: Point):
78 self.angle = get_angle_degrees(*self.position, *point)
79
80
81class GameView(arcade.View):
82
83 def __init__(self):
84 super().__init__()
85
86 # Set Background to be green
87 self.window.background_color = arcade.csscolor.SEA_GREEN
88
89 # The tank and barrel sprite
90 self.tank = arcade.Sprite(TANK_BODY)
91 self.tank.position = self.window.center
92
93 self.barrel = RotatingSprite(TANK_BARREL)
94 self.barrel.position = (
95 self.tank.center_x, self.tank.center_y - TANK_BARREL_LENGTH_HALF
96 )
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 "If the turret rotation is incorrect press P to reset it",
109 self.window.center_x, WINDOW_HEIGHT - 25,
110 anchor_x='center')
111
112 self.control_text = arcade.Text(
113 "WASD to move tank, Mouse to aim",
114 self.window.center_x, 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 += (
135 TANK_TURN_SPEED_DEGREES * self.tank_turning * delta_time
136 )
137
138 # Calculate how much the tank should move forward or back
139 move_magnitude = self.tank_direction * TANK_SPEED_PIXELS * delta_time
140 x_dir = math.sin(self.tank.radians) * move_magnitude
141 y_dir = math.cos(self.tank.radians) * move_magnitude
142
143 # Move the tank's body
144 self.tank.position = (
145 self.tank.center_x + x_dir,
146 self.tank.center_y + y_dir
147 )
148
149 # Move the barrel with the body
150 self.barrel.position = (
151 self.barrel.center_x + x_dir,
152 self.barrel.center_y + y_dir
153 )
154
155 # Begin rotating the barrel by finding the angle to the mouse
156 mouse_angle = get_angle_degrees(
157 self.tank.center_x, self.tank.center_y,
158 self.mouse_pos[0], self.mouse_pos[1])
159
160 # Compensate for fact that the barrel sits horzontally rather than virtically
161 mouse_angle -= 90
162
163 # Rotate the barrel sprite with one end at the tank's center
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
169 def on_key_press(self, symbol: int, modifiers: int):
170 if symbol == arcade.key.W:
171 self.tank_direction += 1
172 elif symbol == arcade.key.S:
173 self.tank_direction -= 1
174 elif symbol == arcade.key.A:
175 self.tank_turning -= 1
176 elif symbol == arcade.key.D:
177 self.tank_turning += 1
178 elif symbol == arcade.key.P:
179 angle = self.barrel.angle
180 self.barrel.angle = 0
181 self.barrel.position = (
182 self.tank.center_x,
183 self.tank.center_y - TANK_BARREL_LENGTH_HALF
184 )
185 self.barrel.rotate_around_point(self.tank.position, angle)
186
187 def on_key_release(self, symbol: int, modifiers: int):
188 if symbol == arcade.key.W:
189 self.tank_direction -= 1
190 elif symbol == arcade.key.S:
191 self.tank_direction += 1
192 elif symbol == arcade.key.A:
193 self.tank_turning += 1
194 elif symbol == arcade.key.D:
195 self.tank_turning -= 1
196
197 def on_mouse_motion(self, x: int, y: int, dx: int, dy: int):
198 self.mouse_pos = x, y
199
200 @property
201 def correct(self):
202 return self._correct
203
204 @correct.setter
205 def correct(self, correct: bool):
206 """
207 Move the tank's barrel between correct rotation and incorrect positions
208 """
209 self._correct = correct
210 if correct:
211 angle = get_angle_radians(
212 self.tank.center_y, self.tank.center_x,
213 self.mouse_pos[1], self.mouse_pos[0])
214
215 self.barrel.position = (
216 self.barrel.center_x + math.sin(angle) * TANK_BARREL_LENGTH_HALF,
217 self.barrel.center_y + math.cos(angle) * TANK_BARREL_LENGTH_HALF,
218 )
219
220 else:
221 self.barrel.position = self.tank.position
222
223
224def main():
225 window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
226 game = GameView()
227
228 window.show_view(game)
229 window.run()
230
231
232if __name__ == '__main__':
233 main()