Move By Keyboard, Fire Towards Mouse

Screen shot of a player controlled tank with rotating barrel

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()