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