Grid Movement
This is a post in the
Worm series.
Other posts in this series:
- May 02, 2023 - Basic Pygame game structure
- May 03, 2023 - Add grid lines and Kinematic objects
- May 21, 2023 - Creating and Moving our player
- May 22, 2023 - Worm eats food
- May 24, 2023 - Grid Movement
- May 30, 2023 - Screen wrap & Detect edges
Grid Movement
There is a few different ways to do this and each way gives you slightly different results.
I wanted a method that would detect when I crossed the center of a tile or landed on it.
I came up with this for going left - I could have started with right, up, or down I guess.
This is debug code and simply prints out position and if center was crossed. If you tried this you’d likely come up with a different solution.
if velocity[0] != 0:
if d < 0:
d = velocity[0]
p = self.rect.center[0]
t = int(p/40) * 40 + 20
e = p + d
o = e - t
print(d,p,t,e,o) #debug
if p > t and t >= e:
print(p, "crossed", t," by ", o ) #debug
in the code above we are looking at the x axis and at the current frame
-
d is a distance in any direction (x axis)
-
p is the player center at the start of the move (frame)
-
t is the center of the tile the player is on (x axis)
-
e is end point of the move (frame)
-
o is how far we will be from the current tile center point or overflow on trigger
Note: I had to explain what each variable was for. Insufficient naming can require extra explaining.
This test sees if we land on or go past the center point. If you wanted to include start from you’d need to change “p > t” to “p >= t”
So for all directions we can reuse some code
def set_next_move(self, velocity):
# python scope - look it up!
if velocity[1] != 0:
d = velocity[1]
p = self.rect.center[1]
else:
d = velocity[0]
p = self.rect.center[0]
t = int(p/40) * 40 + 20
e = p + d
o = e - t
print(d,p,t,e,o)
if d > 0 and p < t and t <= e:
print(p, "crossed", t," by ", o )
if d < 0 and p > t and t >= e:
print(p, "crossed", t," by ", o )
It can be called from move using:
def move(self, dir, dt , speed):
self.speed = speed
dist = int(dt * speed)
velocity = (dir[0] * dist), (dir[1] * dist)
self.test_for_end_of_move(velocity)
self.rect = self.rect.move(velocity)
The code above doesn’t do anything for the player yet, but gives the ‘coder’ some useful feedback. I broke up move for more clarity.
The output looks like this
-3 353 340 350 10
-3 350 340 347 7
-3 347 340 344 4
-3 344 340 341 1
-3 341 340 338 -2
341 crossed 340 by -2
-3 338 340 335 -5
-3 335 340 332 -8
This kind of debugging gives a clear view of what is happening in the code.
The next thing we want to do is to use this to make the grid movement work.
- When we cross a point, but direction hasn’t changed, we want to keep moving.
- When direction has changed once we meet the tile center, we want to move from the tile center to the next point by the “o” amount or overflow in the new direction. i.e. no random delays in turns.
NOTE:
If we wanted our player to stop on center we would ignore the overflow.
For Grid movement to work we need to keep track of the last direction moved
last_dir = [0,0]
We want to have a way to use that tile center calculation, when we adjust player for overflow.
def calc_current_tile_axis_center(self, pos_axis):
return int(pos_axis/40) * 40 + 20
Looking at the git changes we can see what I did just a little better
The full changes of Kinematic Object
class KinematicObject(GameObject):
last_dir = [0,0]
def __init__(self, img_name, initial_pos):
super().__init__(img_name, initial_pos)
def calc_current_tile_axis_center(self, pos_axis):
return int(pos_axis/40) * 40 + 20
def set_next_move(self, velocity):
# python scope - look it up!
if velocity[1] != 0:
d = velocity[1]
p = self.rect.center[1]
else:
d = velocity[0]
p = self.rect.center[0]
t = self.calc_current_tile_axis_center(p)
e = p + d
o = e - t
if d > 0 and p < t and t <= e:
return [True, o]
if d < 0 and p > t and t >= e:
return [True, o]
return [False, o]
def move(self, dir_req, dt , speed):
self.speed = speed
dist = int(dt * speed)
if self.last_dir[0] == 0 and self.last_dir[1] == 0:
self.last_dir = dir_req
dir = dir_req
else:
dir = self.last_dir
velocity = (dir[0] * dist), (dir[1] * dist)
passed_center, overshoot = self.set_next_move(velocity)
if passed_center:
self.rect.center = (self.calc_current_tile_axis_center(self.rect.center[0]), self.calc_current_tile_axis_center(self.rect.center[1]))
self.last_dir = dir_req
dir = dir_req
velocity = (dir[0] * abs(overshoot), dir[1] * abs(overshoot))
self.rect = self.rect.move(velocity)
Great that seems to work surprisingly well. This code breaks when moving on the other side of X or Y axis (i.e. graph or quadrant position).
Create the tail
The first step is to simply create a tailpiece wherever the player eats food.
We create a basic “GameObject” Tail class
class Tail(GameObject):
speed = 0.2
def __init__(self, initial_pos, speed):
super().__init__("assets/player/blue_body_circle.png", initial_pos)
self.speed = speed
Because the snake/player ‘owns’ the tail we modify player with a list
class Player(KinematicObject):
speed = 0.2
tailpieces = []
and we need to add a function to add tailpieces (in player)
def grow_tail(self, initial_pos):
t = Tail(initial_pos, self.speed)
self.tailpieces.append(t)
and finally we need to draw the new objects (in player)
def draw(self):
super().draw()
for t in self.tailpieces:
t.draw()
Now in the game loop we just need to call the function every time the player collides with food
if Rect.collidepoint(food.rect, player.rect.center):
player.grow_tail(food.rect.center)
food.reposition()
Run this, and you will see tailpieces be left behind eat time we eat food.
Previous Next