DISCLAIMER: I don't know the best way or 'right' way to do this. I just cobbled something together and it works nice.
In this update, we'll get rotating turrets. Now, the way I'm doing it is to have 8 images prerotated as opposed to rotating in game. This allows for the rotation algorithm to not skew the pixels too badly and as far as I can tell, it's favored to do it this way instead. Generally, you can get away with a rotated sprite facing each of the cardinal directions. So, that's what I've done.
However, I would like this to be a fun learning adventure for you readers as well so we are going to work through the problem piece by piece. Hopefully when finished, you can have your own turrets in your games that rotate accordingly. Sound fun? I think so!
The first thing to understand when it comes to rotation is that we are operating on an origin of the screen that is not the topleft pixel and thus, we are using polar coordinates. A polar coordinate describes the vector's angle and length from the origin. Our origin will be the center of the screen for now but eventually will be wherever the turret is located.
So, dig yourself up an 8direction sprite that you want to rotate and let's get cracking! First, we need to see what we're dealing with so here's a program that puts the sprite at the center of the screen and draws lines out from it for each of the cardinal directions (no angles yet!)
Starting with our skeleton (in case you aren't keeping up with my work):
And we have a lovely blank screen. Let's draw a circle the size of our screen (so we'll use CENTER_Y as our radius):
Now, we want to draw lines for each of the cardinal directions so to do that we can easily just create a list of coordinates. Here's what they are:
To do this, we scale our vector by the radius of the circle (the vector's magnitude becomes the radius of the circle). To do that, you just multiply each component by the radius. So, here's the code to do that:
So, at the top, you have each of the directions: directions = [(1,1),(0,1),(1,1),(1,0),(1,0),(1,1),(0,1),(1,1)].
Now, of course we could have used (0.5,0.5) instead of (1,1) to keep them unit vectors and avoid having to normalize our vectors but then we couldn't reuse this for our movement calculations if we want to. Do it how you want.
Anyway, to the point of it  we first normalize our vector which turns it into a unit vector. This is done by first finding the magnitude of the vector (which is just its length  and the vector itself is the hypotenuse of a right triangle so we use pythagorean's theorem) and then dividing each component by the magnitude. Viola! unit vector. Simple.
Next, we take our unit vector and scale it by the radius so that we have a magnitude of the radius itself.
Finally, we offset the screen (remember everything starts 0,0 at topleft) by adding the SCREENCENTER's location to our new location which is done by adding X to X and Y to Y.
We draw the line  do it for each direction  and this is what we get:
So, that's pretty cool right? Take a break and go create that for yourself. There's more.
See any problems? What are the boundaries of the sprite's sight  how do you know when to switch the sprite image? There's no clearcut boundaries and so what we do now is divide the circle on an offset. What offset you ask? Well, we have 8 directions which makes it 360/8 = 45. Half of 45 is 22.5. Therefore, our offset should be 22.5 degrees but we continue to plot every 45 degrees of course.
So, with that in mind, I created this new code that draws those lines in red. Here's what I came up with:
I introduced a few functions here. ptoc() converts polar coordinates to cartesian coordinates and get_angle() takes two cartesian coordinates and finds out the angle.
In terms of the loop, I start it at 22 and loop by 45 degrees until reaching 360. What pops up is this:
And you'll notice that I had it print the values as well so here's those:
we can now use those for our if statements to check which sprite to use. Could this have all been figured out without doing this stuff? Sure, it's pretty selfexplanatory but this is the way we learn  by doing the small steps. We now have visual aids describing how the sprite should know where it's supposed to look too.
I created the base to work with here:
I won't get into sprites here. They're actually a little weird to work with  and my transparency isn't working but that's ok... beside the point.
Anyway, we now have to fill in the update with the correct view. I figure we'll just have the plane track the mouse pointer. Here's the update code in the sprite class:
Here's the main code:
Don't forget to add self.target = SCREENCENTER to the sprite class's init constructor. Hope you had fun.
In this update, we'll get rotating turrets. Now, the way I'm doing it is to have 8 images prerotated as opposed to rotating in game. This allows for the rotation algorithm to not skew the pixels too badly and as far as I can tell, it's favored to do it this way instead. Generally, you can get away with a rotated sprite facing each of the cardinal directions. So, that's what I've done.
However, I would like this to be a fun learning adventure for you readers as well so we are going to work through the problem piece by piece. Hopefully when finished, you can have your own turrets in your games that rotate accordingly. Sound fun? I think so!
The first thing to understand when it comes to rotation is that we are operating on an origin of the screen that is not the topleft pixel and thus, we are using polar coordinates. A polar coordinate describes the vector's angle and length from the origin. Our origin will be the center of the screen for now but eventually will be wherever the turret is located.
So, dig yourself up an 8direction sprite that you want to rotate and let's get cracking! First, we need to see what we're dealing with so here's a program that puts the sprite at the center of the screen and draws lines out from it for each of the cardinal directions (no angles yet!)
Starting with our skeleton (in case you aren't keeping up with my work):
import math import pygame from pygame.locals import * SCREENSIZE = SCREEN_X, SCREEN_Y = (640,480) SCREENCENTER = CENTER_X, CENTER_Y = (SCREEN_X/2, SCREEN_Y/2) X = 0 Y = 1 def main(): pygame.init() screen = pygame.display.set_mode(SCREENSIZE) in_game = True while in_game: for event in pygame.event.get(): if event.type == QUIT: in_game = False pygame.display.flip() if __name__ == '__main__': main()
And we have a lovely blank screen. Let's draw a circle the size of our screen (so we'll use CENTER_Y as our radius):
import math import pygame from pygame.locals import * SCREENSIZE = SCREEN_X, SCREEN_Y = (640,480) SCREENCENTER = CENTER_X, CENTER_Y = (SCREEN_X/2, SCREEN_Y/2) X = 0 Y = 1 GREEN = (0,255,0) def main(): pygame.init() screen = pygame.display.set_mode(SCREENSIZE) in_game = True while in_game: for event in pygame.event.get(): if event.type == QUIT: in_game = False pygame.draw.circle(screen, # Surface GREEN, # Color SCREENCENTER, # Position int(CENTER_Y), # Radius 1) # Width pygame.display.flip() if __name__ == '__main__': main()
Now, we want to draw lines for each of the cardinal directions so to do that we can easily just create a list of coordinates. Here's what they are:
""" (1,1),(0,1),(1,1) NW N NE \/ (1,0) W*E (1,0) /\ SW S SE (1,1),(0,1),(1,1) """
To do this, we scale our vector by the radius of the circle (the vector's magnitude becomes the radius of the circle). To do that, you just multiply each component by the radius. So, here's the code to do that:
import math import pygame from pygame.locals import * SCREENSIZE = SCREEN_X, SCREEN_Y = (640,480) SCREENCENTER = CENTER_X, CENTER_Y = (SCREEN_X/2, SCREEN_Y/2) X = 0 Y = 1 GREEN = (0,255,0) directions = [(1,1),(0,1),(1,1),(1,0),(1,0),(1,1),(0,1),(1,1)] def normalize((x, y)): mag = get_magnitude((x, y)) if mag > 0: return (x / mag, y / mag) def get_magnitude((x, y)): a = x**2.0 b = y**2.0 c = math.sqrt(a + b) return c def add((x, y), (dx, dy)): return ((x + dx), (y + dy)) def sub((x, y), (dx, dy)): return ((x  dx), (y  dy)) def mul((x, y), scale): return (float(x * scale), float(y * scale)) def main(): pygame.init() screen = pygame.display.set_mode(SCREENSIZE) in_game = True while in_game: for event in pygame.event.get(): if event.type == QUIT: in_game = False pygame.draw.circle(screen, # Surface GREEN, # Color SCREENCENTER, # Position int(CENTER_Y), # Radius 1) # Width for direction in directions: normal = normalize(direction) line = mul(normal, CENTER_Y) plot = add(SCREENCENTER, line) pygame.draw.line(screen, GREEN, SCREENCENTER, plot, 1) pygame.display.flip() if __name__ == '__main__': main()
So, at the top, you have each of the directions: directions = [(1,1),(0,1),(1,1),(1,0),(1,0),(1,1),(0,1),(1,1)].
Now, of course we could have used (0.5,0.5) instead of (1,1) to keep them unit vectors and avoid having to normalize our vectors but then we couldn't reuse this for our movement calculations if we want to. Do it how you want.
Anyway, to the point of it  we first normalize our vector which turns it into a unit vector. This is done by first finding the magnitude of the vector (which is just its length  and the vector itself is the hypotenuse of a right triangle so we use pythagorean's theorem) and then dividing each component by the magnitude. Viola! unit vector. Simple.
Next, we take our unit vector and scale it by the radius so that we have a magnitude of the radius itself.
Finally, we offset the screen (remember everything starts 0,0 at topleft) by adding the SCREENCENTER's location to our new location which is done by adding X to X and Y to Y.
We draw the line  do it for each direction  and this is what we get:
So, that's pretty cool right? Take a break and go create that for yourself. There's more.
See any problems? What are the boundaries of the sprite's sight  how do you know when to switch the sprite image? There's no clearcut boundaries and so what we do now is divide the circle on an offset. What offset you ask? Well, we have 8 directions which makes it 360/8 = 45. Half of 45 is 22.5. Therefore, our offset should be 22.5 degrees but we continue to plot every 45 degrees of course.
So, with that in mind, I created this new code that draws those lines in red. Here's what I came up with:
import math import pygame from pygame.locals import * SCREENSIZE = SCREEN_X, SCREEN_Y = (640,480) SCREENCENTER = CENTER_X, CENTER_Y = (SCREEN_X/2, SCREEN_Y/2) X = 0 Y = 1 RED = (255,0,0) GREEN = (0,255,0) directions = [(1,1),(0,1),(1,1),(1,0),(1,0),(1,1),(0,1),(1,1)] def ptoc((a, r)): a = float(a) r = float(r) y = math.sin(a) * r x = math.cos(a) * r return (x, y) def get_angle(origin, location): dx = float(location[0])  float(origin[0]) dy = (float(location[1])  float(origin[1])) r = math.atan2(dy, dx) r %= 2 * math.pi deg = math.degrees(r) return (deg, r) def normalize((x, y)): mag = get_magnitude((x, y)) if mag > 0: return (x / mag, y / mag) def get_magnitude((x, y)): a = x**2.0 b = y**2.0 c = math.sqrt(a + b) return c def add((x, y), (dx, dy)): return ((x + dx), (y + dy)) def sub((x, y), (dx, dy)): return ((x  dx), (y  dy)) def mul((x, y), scale): return (float(x * scale), float(y * scale)) def main(): pygame.init() screen = pygame.display.set_mode(SCREENSIZE) values_printed = False in_game = True while in_game: for event in pygame.event.get(): if event.type == QUIT: in_game = False pygame.draw.circle(screen, # Surface GREEN, # Color SCREENCENTER, # Position int(CENTER_Y), # Radius 1) # Width for direction in directions: normal = normalize(direction) line = mul(normal, CENTER_Y) plot = add(SCREENCENTER, line) pygame.draw.line(screen, GREEN, SCREENCENTER, plot, 1) for i in xrange(22, 360, 45): offset = math.radians(i + .5) location = ptoc((offset, CENTER_Y)) # convert polar to cartesian normal = normalize(location) # normalize the vector line = mul(normal, CENTER_Y) # scale unit vector by radius plot = add(SCREENCENTER, line) # true location to plot pygame.draw.line(screen, RED, SCREENCENTER, plot, 1) if not values_printed: print i values_printed = True pygame.display.flip() if __name__ == '__main__': main()
I introduced a few functions here. ptoc() converts polar coordinates to cartesian coordinates and get_angle() takes two cartesian coordinates and finds out the angle.
In terms of the loop, I start it at 22 and loop by 45 degrees until reaching 360. What pops up is this:
And you'll notice that I had it print the values as well so here's those:
22 23 68 113 158 203 248 293 338
we can now use those for our if statements to check which sprite to use. Could this have all been figured out without doing this stuff? Sure, it's pretty selfexplanatory but this is the way we learn  by doing the small steps. We now have visual aids describing how the sprite should know where it's supposed to look too.
I created the base to work with here:
import math import pygame from pygame.locals import * SCREENSIZE = SCREEN_X, SCREEN_Y = (640,480) SCREENCENTER = CENTER_X, CENTER_Y = (SCREEN_X/2, SCREEN_Y/2) X = 0 Y = 1 RED = (255,0,0) GREEN = (0,255,0) directions = [(1,1),(0,1),(1,1),(1,0),(1,0),(1,1),(0,1),(1,1)] def ptoc((a, r)): a = float(a) r = float(r) y = math.sin(a) * r x = math.cos(a) * r return (x, y) def get_angle(origin, location): dx = float(location[0])  float(origin[0]) dy = (float(location[1])  float(origin[1])) r = math.atan2(dy, dx) r %= 2 * math.pi deg = math.degrees(r) return (deg, r) def normalize((x, y)): mag = get_magnitude((x, y)) if mag > 0: return (x / mag, y / mag) def get_magnitude((x, y)): a = x**2.0 b = y**2.0 c = math.sqrt(a + b) return c def add((x, y), (dx, dy)): return ((x + dx), (y + dy)) def sub((x, y), (dx, dy)): return ((x  dx), (y  dy)) def mul((x, y), scale): return (float(x * scale), float(y * scale)) class RotatingSprite(pygame.sprite.Sprite): def __init__(self, group): pygame.sprite.Sprite.__init__(self, group) self.images = { 'north' : pygame.image.load('res/orange_n.png').convert(), 'ne' : pygame.image.load('res/orange_ne.png').convert(), 'nw' : pygame.image.load('res/orange_nw.png').convert(), 'south' : pygame.image.load('res/orange_s.png').convert(), 'se' : pygame.image.load('res/orange_se.png').convert(), 'sw' : pygame.image.load('res/orange_sw.png').convert(), 'west' : pygame.image.load('res/orange_w.png').convert(), 'east' : pygame.image.load('res/orange_e.png').convert() } # add transparency for (key,value) in self.images.iteritems(): value = value.set_colorkey((2, 73, 148)) self.image = self.images['south'] self.rect = self.image.get_rect() self.rect.move_ip(sub(SCREENCENTER, self.rect.center)) def update(self): pass def main(): pygame.init() screen = pygame.display.set_mode(SCREENSIZE) background = pygame.Surface(screen.get_size()) group = pygame.sprite.Group() sprite = RotatingSprite(group) in_game = True while in_game: for event in pygame.event.get(): if event.type == QUIT: in_game = False pygame.draw.circle(screen, # Surface GREEN, # Color SCREENCENTER, # Position int(CENTER_Y), # Radius 1) # Width for direction in directions: normal = normalize(direction) line = mul(normal, CENTER_Y) plot = add(SCREENCENTER, line) pygame.draw.line(screen, GREEN, SCREENCENTER, plot, 1) for i in xrange(22, 360, 45): offset = math.radians(i + .5) location = ptoc((offset, CENTER_Y)) # convert polar to cartesian normal = normalize(location) # normalize the vector line = mul(normal, CENTER_Y) # scale unit vector by radius plot = add(SCREENCENTER, line) # true location to plot pygame.draw.line(screen, RED, SCREENCENTER, plot, 1) group.clear(screen,background) group.update() group.draw(screen) pygame.display.flip() if __name__ == '__main__': main()
I won't get into sprites here. They're actually a little weird to work with  and my transparency isn't working but that's ok... beside the point.
Anyway, we now have to fill in the update with the correct view. I figure we'll just have the plane track the mouse pointer. Here's the update code in the sprite class:
def update(self): deg,rad = get_angle(self.rect.center, self.target) if 337.5 <= deg or deg < 22.5: self.image = self.images['east'] elif 22.5 <= deg < 67.5: self.image = self.images['ne'] elif 67.5 <= deg < 112.5: self.image = self.images['north'] elif 112.5 <= deg < 157.5: self.image = self.images['nw'] elif 157.5 <= deg < 202.5: self.image = self.images['west'] elif 202.5 <= deg < 247.5: self.image = self.images['sw'] elif 247.5 <= deg < 292.5: self.image = self.images['south'] elif 292.5 <= deg < 337.5: self.image = self.images['se'] else: print 'ERROR: %r' % deg
Here's the main code:
while in_game: for event in pygame.event.get(): if event.type == QUIT: in_game = False if event.type == MOUSEMOTION: sprite.target = pygame.mouse.get_pos()
Don't forget to add self.target = SCREENCENTER to the sprite class's init constructor. Hope you had fun.
0 Comments On This Entry
Trackbacks for this entry [ Trackback URL ]
Tags
My Blog Links
Recent Entries
Recent Comments
Search My Blog
0 user(s) viewing
0 Guests
0 member(s)
0 anonymous member(s)
0 member(s)
0 anonymous member(s)
