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)