Sprite Rotation Around a Tank#

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