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.