Sprite Rotation Around a Tank#

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