Posts
Search
Contact
Cookies
About
RSS

Python and raylib pushing crates

Added 1 Oct 2020, 8:38 a.m. edited 18 Jun 2023, 1:12 a.m.

Believe it or not we're not a million miles away from a functioning game! By using the players isWalkable function, we can push crates around when its appropriate. But first we need to set up our crates.

# returns the current level map
def setupLevel():
    global crates, level, levels
    crates=[]
    # making a deep copy allows the level to be reset from levels
    l = copy.deepcopy(levels[level])
    for y in range(0,11):
        for x in range(0,20):
            for i in range(0,3):        # layers
                if i < len(l[x][y]):    # check layer exists (some cells might only
                                        # have something on bottom layer)
                    
                    t = l[x][y][i]      

                    if t == 55:                     # player start marker
                        playerPos.x = x * 64
                        playerPos.y = y * 64
                        l[x][y][i] = 0              # remove marker
                        
                    if t > 0 and t < 11:            # crate marker
                        Crate(x*64,y*64,t)          # don't keep the instance its put on the list
                        l[x][y][i] = 0              # remove marker
    return l

What's happening here isn't too complex - we're taking the map template for the new level and looking through it. If we find a tile representing a crate (tile 1-10) then we remove that marker and create a new Crate

        
class Crate:
    # crates have a movement behaviour
    def __init__(self,x,y,tile):
        global crates
        self.pos = ffi.new("struct Vector2 *",[ x, y])
        self.tile = tile
        self.isMoving = Moving.NOT
        crates.append(self)

Notice when creating a new crate we have a global crates variable, this is a list of all the active crates, useful when we want to iterate them to update and draw them. The crate update handles the crate movement after the player has pushed it. Before looking at the isWalkable function - which has ended up becoming fundamental to the way the game works, lets see how the player update function calls it....

        # if the way is clear move in the direction requested
        if rl.IsKeyDown(rl.KEY_UP) and isWalkable(px,py-1, Moving.UP, 2):
            lisMoving = Moving.UP
            lmoveCount = 32

The key here is the last parameter, set to two this means we are looking at the square next to us and for the sake a any crate we are near, the next square too, if both squares are clear even if we are near a crate then we can move into this square, setting the crate moving if needed. Without controlling the number of times the function can call itself (the 2) then we'd end up being able to push a whole row of crates which we don't want!

The fist part of isWalkable is for the benefit of the player, the second part involving getting crates moving if applicable.

We now have the basics of a playable game, but what we need now is the fineness for example some kind of menu at the start, better transition between levels, and proper end state (if you mange to complete all the levels)

No you have a complete base of a game however, I'll leave you to add a menu and generally make it behave nicer!

 #!/usr/bin/env python3
import json

from raylib.static import rl, ffi
from raylib.colors import *

from enum import Enum
import copy


# these are the different movement states for the player and crates
class Moving(Enum):
    NOT = 0
    UP = 1
    DOWN = 2
    LEFT = 3
    RIGHT = 4
    
    # for debugging, allows printing of human readable version of value
    def __str__(self):
        if self.value == 0: return "NOT"
        if self.value == 1: return "UP"
        if self.value == 2: return "DOWN"
        if self.value == 3: return "LEFT"
        if self.value == 4: return "RIGHT"
        return "???"

        
class Crate:
    # crates have a movement behaviour
    def __init__(self,x,y,tile):
        global crates
        self.pos = ffi.new("struct Vector2 *",[ x, y])
        self.tile = tile
        self.isMoving = Moving.NOT
        crates.append(self)
    
    def update(self):
        if self.isMoving == Moving.DOWN:
            self.pos.y += 2
        if self.isMoving == Moving.UP:
            self.pos.y -= 2
        if self.isMoving == Moving.RIGHT:
            self.pos.x += 2
        if self.isMoving == Moving.LEFT:
            self.pos.x -= 2
        
        if self.isMoving != Moving.NOT:
            self.moveCount -= 1
            if self.moveCount == 0:
                self.isMoving = Moving.NOT
                
    # sets a crate moving
    def setMoving(self, d):
        self.isMoving = d
        self.moveCount = 32


# laFr is current animation frame, lisMoving is movement direction of crate, moving count down
def updatePlayer(laFr, lisMoving, lmoveCount):
    global plplayerPos
    fi = 55
    
    # if moving update the move counter
    if lisMoving != Moving.NOT:
        fi += laFr
        lmoveCount -= 1
        if lmoveCount == 0:
            lisMoving = Moving.NOT
    
    px = playerPos.x / 64
    py = playerPos.y / 64
    # if not moving check for movement key presses
    if lisMoving == Moving.NOT:
        # if the way is clear move in the direction requested
        if rl.IsKeyDown(rl.KEY_UP) and isWalkable(px,py-1, Moving.UP, 2):
            lisMoving = Moving.UP
            lmoveCount = 32
        if rl.IsKeyDown(rl.KEY_DOWN) and isWalkable(px,py+1, Moving.DOWN, 2):
            lisMoving = Moving.DOWN
            lmoveCount = 32
        if rl.IsKeyDown(rl.KEY_RIGHT) and isWalkable(px+1,py, Moving.RIGHT, 2):
            lisMoving = Moving.RIGHT
            lmoveCount = 32
        if rl.IsKeyDown(rl.KEY_LEFT) and isWalkable(px-1,py, Moving.LEFT, 2):
            lisMoving = Moving.LEFT
            lmoveCount = 32
    
    # change player position and animation depending on movement direction
    if lisMoving == Moving.DOWN:
        playerPos.y += 2
    if lisMoving == Moving.UP:
        playerPos.y -= 2
        fi += 3
    if lisMoving == Moving.RIGHT:
        playerPos.x += 2
        fi += 22
    if lisMoving == Moving.LEFT:
        playerPos.x -= 2
        fi += 25
    
    return fi, lisMoving, lmoveCount

# takes a texture tile from an image sprite sheet
def getTxFromSheet(source, ix:int , iy:int):
    ii = rl.ImageFromImage(source, [ix*64,iy*64,64,64])
    tx = rl.LoadTextureFromImage(ii)
    rl.UnloadImage(ii)
    return tx

# when a new level starts check through the level
# for markers for player start and crates
# returns the current level map
def setupLevel():
    global crates, level, levels
    crates=[]
    # making a deep copy allows the level to be reset from levels
    l = copy.deepcopy(levels[level])
    for y in range(0,11):
        for x in range(0,20):
            for i in range(0,3):        # layers
                if i < len(l[x][y]):    # check layer exists (some cells might only
                                        # have something on bottom layer)
                    
                    t = l[x][y][i]      

                    if t == 55:                     # player start marker
                        playerPos.x = x * 64
                        playerPos.y = y * 64
                        l[x][y][i] = 0              # remove marker
                        
                    if t > 0 and t < 11:            # crate marker
                        Crate(x*64,y*64,t)          # don't keep the instance its put on the list
                        l[x][y][i] = 0              # remove marker
    return l

# is cell ix, iy clear of barriers, idir is the direction of movement
def isWalkable(ix, iy, idir, it):
    global crates, clevel
    ix=int(ix)
    iy=int(iy)
    t = clevel[ix][iy][0]
    if t != 0 and t != 76 and t !=87:     # if layer 0 is empty floor tile or floor target
        return False
    if len(clevel[ix][iy]) == 2:          # layer 1 blocks if not empty
        if clevel[ix][iy][1] != 0:
            return False
    if len(clevel[ix][iy]) == 3:          # layer 2 blocks if not empty
        if clevel[ix][iy][2] != 0:
            return False
    
    # bail if 3rd iterration we don't want a crate to push a crate
    if it == 0: return False
    # check for crate and start moving is it can
    for c in crates:
        # crate position in level coordinates
        cpx = int(c.pos.x / 64)
        cpy = int(c.pos.y / 64)
        if cpx == ix and cpy == iy:  # have a crate to push
            # check crate can move
            if idir == Moving.UP and not isWalkable(ix,iy-1,Moving.UP, it-1): return False
            if idir == Moving.DOWN and not isWalkable(ix,iy+1,Moving.DOWN, it-1): return False
            if idir == Moving.LEFT and not isWalkable(ix-1,iy,Moving.LEFT, it-1): return False
            if idir == Moving.RIGHT and not isWalkable(ix+1,iy,Moving.RIGHT, it-1): return False
            # pushing a crate that can move    
            c.setMoving(idir)
    # allow player to follow moving crate        
    return True
        
    
    
#// Initialization
#//--------------------------------------------------------------------------------------
screenWidth = 1280;
screenHeight = 720;

rl.SetConfigFlags(rl.FLAG_MSAA_4X_HINT)  # Enable Multi Sampling Anti Aliasing 4x (if available)
rl.SetTargetFPS(60)
rl.InitWindow(screenWidth, screenHeight, b"step 6")

sheet = rl.LoadImage(b"data/sokoban_tilesheet.png")
# load in all the tiles from the tile sheet...
tiles = []
for y in range(0,8):
    for x in range(0,11):
        tiles.append(getTxFromSheet(sheet,x,y))                   
rl.UnloadImage(sheet)

# load all the levels
with open('levels.json', 'r') as file:
    levels = json.loads(file.read())

# count for the frame and slower count for animating the player
frame = 0
aFr = 0

isMoving = Moving.NOT      # player movement
moveCount = 0              # player movement count down
playerPos = ffi.new("struct Vector2 *",[ 6*32, 6*32])

level=0                    # which level is it
crates=[]                  # list of crates on level
clevel = setupLevel()      # current level map


while not rl.WindowShouldClose():            #// Detect window close button or ESC key
    #// Update
    #//----------------------------------------------------------------------------------

    frame+=1
    if not(frame % 6):    # every 6th frame
        aFr+=1            # increment player frame
        if aFr == 3:      # does it need to wrap (return to zero)
            aFr=0

    # update the player tells us the animation frame for the player
    # movement status and the count through the move
    playerFrame, isMoving, moveCount = updatePlayer(aFr, isMoving, moveCount)
    
    # update crates (some might be moving) and check for level complete
    done = True
    for c in crates:
        c.update()
        cx = int(c.pos.x / 64)
        cy = int(c.pos.y / 64)
        if clevel[cx][cy][0] != 87: done = False
    
    # TODO check for end of levels...
    if done:
        level += 1
        clevel = setupLevel()
    
    # if the user decides they can't complete the level
    # allow them to reset it
    if rl.IsKeyPressed(rl.KEY_R):
        clevel = setupLevel()
        
    #// Draw
    #//----------------------------------------------------------------------------------
    rl.BeginDrawing()
    rl.ClearBackground(BLACK)

    # draw the level (walls and floor)
    for y in range(0,11):
        for x in range(0,20):
            for i in range(0,3):
                if i < len(levels[level][x][y]):
                    rl.DrawTexture(tiles[clevel[x][y][i]], x*64, y*64, WHITE)
    
    # draw crates
    for c in crates:
        rl.DrawTexture(tiles[c.tile], int(c.pos.x), int(c.pos.y), WHITE)     
    
    # draw the player
    rl.DrawTexture(tiles[playerFrame], int(playerPos.x), int(playerPos.y), WHITE)
    rl.DrawText(rl.TextFormat(b"level: %i",ffi.cast("int", level)), 10, screenHeight-30, 20, WHITE)

    rl.EndDrawing()

for i in tiles:
    rl.UnloadTexture(i)