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
 42SCREEN_WIDTH = 800
 43SCREEN_HEIGHT = 600
 44SCREEN_MIDDLE = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
 45
 46
 47SCREEN_TITLE = "Rotating Tank Example"
 48
 49
 50# These paths are built-in resources included with arcade
 51TANK_BODY = ":resources:images/topdown_tanks/tankBody_dark_outline.png"
 52TANK_BARREL = ":resources:images/topdown_tanks/tankDark_barrel3_outline.png"
 53
 54
 55class RotatingSprite(arcade.Sprite):
 56    """
 57    Sprite subclass which can be rotated around a point.
 58
 59    This version of the class always changes the angle of the sprite.
 60    Other games might not rotate the sprite. For example, moving
 61    platforms in a platformer wouldn't rotate.
 62    """
 63    def rotate_around_point(self, point: Point, degrees: float):
 64        """
 65        Rotate the sprite around a point by the set amount of degrees
 66
 67        :param point: The point that the sprite will rotate about
 68        :param degrees: How many degrees to rotate the sprite
 69        """
 70
 71        # Make the sprite turn as its position is moved
 72        self.angle += degrees
 73
 74        # Move the sprite along a circle centered around the passed point
 75        self.position = rotate_point(
 76            self.center_x, self.center_y,
 77            point[0], point[1], degrees)
 78
 79
 80class ExampleWindow(arcade.Window):
 81
 82    def __init__(self):
 83        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
 84
 85        # Set Background to be green
 86        self.background_color = arcade.csscolor.SEA_GREEN
 87
 88        # The tank and barrel sprite
 89        self.tank = arcade.Sprite(TANK_BODY)
 90        self.tank.position = SCREEN_MIDDLE
 91
 92        self.barrel = RotatingSprite(TANK_BARREL)
 93        self.barrel.position =\
 94            SCREEN_MIDDLE[0], SCREEN_MIDDLE[1] - TANK_BARREL_LENGTH_HALF
 95
 96        self.tank_direction = 0.0  # Forward & backward throttle
 97        self.tank_turning = 0.0  # Turning strength to the left or right
 98
 99        self.mouse_pos = 0, 0
100
101        self.tank_sprite_list = arcade.SpriteList()
102        self.tank_sprite_list.extend([self.tank, self.barrel])
103
104        self._correct = True
105        self.correct_text = arcade.Text(
106            "Turret Rotation is Correct, Press P to Switch",
107            SCREEN_MIDDLE[0], SCREEN_HEIGHT - 25,
108            anchor_x='center')
109
110        self.control_text = arcade.Text(
111            "WASD to move tank, Mouse to aim",
112            SCREEN_MIDDLE[0], 15,
113            anchor_x='center')
114
115    def on_draw(self):
116        self.clear()
117        self.tank_sprite_list.draw()
118
119        self.control_text.draw()
120        self.correct_text.draw()
121
122    def on_update(self, delta_time: float):
123        self.move_tank(delta_time)
124
125    def move_tank(self, delta_time):
126        """
127        Perform all calculations for moving the tank's body and barrel
128        """
129
130        # Rotate the tank's body in place without changing position
131        # We'll rotate the barrel after updating the entire tank's x & y
132        self.tank.angle += TANK_TURN_SPEED_DEGREES\
133            * self.tank_turning * delta_time
134
135        # Calculate how much the tank should move forward or back
136        move_magnitude = self.tank_direction * TANK_SPEED_PIXELS * delta_time
137        x_dir = math.sin(self.tank.radians) * move_magnitude
138        y_dir = math.cos(self.tank.radians) * move_magnitude
139
140        # Move the tank's body
141        self.tank.position =\
142            self.tank.center_x + x_dir,\
143            self.tank.center_y + y_dir
144
145        # Move the barrel with the body
146        self.barrel.position =\
147            self.barrel.center_x + x_dir,\
148            self.barrel.center_y + y_dir
149
150        # Begin rotating the barrel by finding the angle to the mouse
151        mouse_angle = get_angle_degrees(
152            self.tank.center_x, self.tank.center_y,
153            self.mouse_pos[0], self.mouse_pos[1])
154
155        # Compensate for the flipped orientation of the barrel texture
156        # This could be skipped if the texture faced up instead
157        mouse_angle += 180
158
159        if self.correct:
160            # Rotate the barrel sprite with one end at the tank's center
161
162            # Subtract the old angle to get the change in angle
163            angle_change = mouse_angle - self.barrel.angle
164
165            self.barrel.rotate_around_point(self.tank.position, angle_change)
166        else:
167            # Swivel the barrel with its center aligned with the body's
168            self.barrel.angle = mouse_angle
169
170    def on_key_press(self, symbol: int, modifiers: int):
171        if symbol == arcade.key.W:
172            self.tank_direction += 1
173        elif symbol == arcade.key.S:
174            self.tank_direction -= 1
175        elif symbol == arcade.key.A:
176            self.tank_turning -= 1
177        elif symbol == arcade.key.D:
178            self.tank_turning += 1
179        elif symbol == arcade.key.P:
180            self.correct = not self.correct
181
182            self.correct_text.text =\
183                f"Turret Rotation is "\
184                f"{'Correct' if self.correct else 'Incorrect'},"\
185                f" Press P to Switch"
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 = ExampleWindow()
226    window.run()
227
228
229if __name__ == '__main__':
230    main()