Python and raylib – a simple 2d level editor…

Python has some nice tools to enable to load and save objects, in particular to a json file.

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

and we have all our levels loaded. In this case its a grid of cells, each cell potentially holding 3 layers. The bottom layer is for walkable tiles and crate targets – this puzzle game has us pushing crates onto target squares. As well as crates there are immovable walls, typically on the middle layer, which is also holding markers for the crate start positions.

I’ll go through the controls of the level editor before presenting the code

the ‘l’ (lower case L !) key takes you to the next level or creates a new one, holding shift and you go backwards a level.

[ & ] allow you to change the current tile to place

Space places the current tile on layer 0, “1” places the current tile on the middle layer “2” places the current tile on the top layer

Holding down the “t” key shows you the whole sprite sheet allowing mouse selection of the current tile

finally pressing “s” saves all the levels…

a fair bit of functionality, and in future the game will rely on certain tile values in the level to place items when the level start.

tile 55 denotes the player start position.

tiles 1-10 denote crates start position

on tiles 76 an 87 are walkable 87 being the crate targets

here is the somewhat rudimentary level editor to play with

#!/usr/bin/env python3
import json

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

from enum import Enum

# these are the different movement states for the player
class Moving(Enum):
    NOT = 0
    UP = 1
    DOWN = 2
    LEFT = 3
    RIGHT = 4


    
def updatePlayer(laFr, lisMoving, lmoveCount):
    fi = 55
    
    # if moving update the move counter
    if lisMoving != Moving.NOT:
        fi += laFr
        lmoveCount -= 1
        if lmoveCount == 0:
            lisMoving = Moving.NOT
            
    # if not moving check for movement key presses
    if lisMoving == Moving.NOT:
        if rl.IsKeyDown(rl.KEY_UP):
            lisMoving = Moving.UP
            lmoveCount = 32
        if rl.IsKeyDown(rl.KEY_DOWN):
            lisMoving = Moving.DOWN
            lmoveCount = 32
        if rl.IsKeyDown(rl.KEY_RIGHT):
            lisMoving = Moving.RIGHT
            lmoveCount = 32
        if rl.IsKeyDown(rl.KEY_LEFT):
            lisMoving = Moving.LEFT
            lmoveCount = 32
    
    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



#// Initialization
#//--------------------------------------------------------------------------------------
screenWidth = 1280;
screenHeight = 720;

rl.SetConfigFlags(rl.FLAG_MSAA_4X_HINT| rl.FLAG_WINDOW_RESIZABLE)  # Enable Multi Sampling Anti Aliasing 4x (if available)

rl.InitWindow(screenWidth, screenHeight, b"step 4 - level editor!")

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

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

frame = 0
aFr = 0

isMoving = Moving.NOT
moveCount = 0
playerPos = ffi.new("struct Vector2 *",[ screenWidth/2, screenHeight/2])
level=0
placeTile=0

rl.SetTargetFPS(60)
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)
    
    #//----------------------------------------------------------------------------------

    #// Draw
    #//----------------------------------------------------------------------------------
    rl.BeginDrawing()
    rl.ClearBackground(GRAY)

    if rl.IsKeyPressed(rl.KEY_L):
        if rl.IsKeyDown(rl.KEY_LEFT_SHIFT) or rl.IsKeyDown(rl.KEY_RIGHT_SHIFT):
            level -= 1
            if level == -1: level = 0
        else:           
            level += 1
            if len(levels) != level+1: 
                l=([
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]],
                    [[0],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]]
                ])
                levels.append(l)
                
    # draw the level if not viewing tiles
    if not rl.IsKeyDown(rl.KEY_T):
        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[levels[level][x][y][i]], x*64, y*64, WHITE)
    
    # [ & ] change selected tile - also use T and mouse
    if rl.IsKeyPressed(rl.KEY_LEFT_BRACKET):
        placeTile-=1
        if placeTile==-1:
            placeTile=87

    if rl.IsKeyPressed(rl.KEY_RIGHT_BRACKET):
        placeTile+=1
        if placeTile==88:
            placeTile=0
            
    # place tile on bottom layer
    if rl.IsKeyPressed(rl.KEY_SPACE):
        while len(levels[level][int(playerPos.x/64)][int(playerPos.y/64)]) < 1:
            levels[level][int(playerPos.x/64)][int(playerPos.y/64)].append(0)
        levels[level][int(playerPos.x/64)][int(playerPos.y/64)][0] = placeTile
    
    # place tile on mid layer
    if rl.IsKeyPressed(rl.KEY_ONE):
        while len(levels[level][int(playerPos.x/64)][int(playerPos.y/64)]) < 2:
            levels[level][int(playerPos.x/64)][int(playerPos.y/64)].append(0)
        levels[level][int(playerPos.x/64)][int(playerPos.y/64)][1] = placeTile

    # place tile on top layer
    if rl.IsKeyPressed(rl.KEY_TWO):
        while len(levels[level][int(playerPos.x/64)][int(playerPos.y/64)]) < 3:
            levels[level][int(playerPos.x/64)][int(playerPos.y/64)].append(0)
        levels[level][int(playerPos.x/64)][int(playerPos.y/64)][2] = placeTile
        
    # if T held down then show the sprite sheet
    if rl.IsKeyDown(rl.KEY_T):
        i=0
        for y in range(0,8):
            for x in range(0,11):
                rl.DrawTexture(tiles[i], x*64, y*64, WHITE)
                rl.DrawText(rl.TextFormat(b"i: %i",ffi.cast("int", i)), x*64, y*64, 20, BLACK)
                i+=1
        # mouse selects tile
        mx = int(rl.GetMouseX()/64)
        my = int(rl.GetMouseY()/64)
        if mx >= 0 and mx < 11 and my >= 0 and my < 8:
            rl.DrawRectangleLines(mx*64,my*64,64,64,RED)
            placeTile = (my * 11) + mx
    
    # save
    if rl.IsKeyPressed(rl.KEY_S):
        text = json.dumps(levels, sort_keys=False, indent=4)
        with open('levels.json', 'w') as f:
            print(text, file=f)
        rl.TraceLog(rl.LOG_INFO,b"SAVED!")
    
    # draw the player
    rl.DrawTexture(tiles[playerFrame], int(playerPos.x), int(playerPos.y), WHITE)
        
    rl.DrawFPS(10, 10)
    #rl.DrawText(f"Frame: {int(frame)}".encode('UTF-8') , 10, 28, 20, MAROON)
    rl.DrawText(rl.TextFormat(b"Frame: %i",ffi.cast("int", frame)), 10, 30, 20, MAROON)
    rl.DrawText(rl.TextFormat(b"a fr : %i",ffi.cast("int", aFr)), 10, 60, 20, MAROON)
    rl.DrawText(b"PLACE", 10, 90, 20, WHITE)
    rl.DrawText(rl.TextFormat(b"tile: %i",ffi.cast("int", placeTile)), 10, 120, 20, WHITE)
    rl.DrawText(rl.TextFormat(b"level: %i",ffi.cast("int", level)), 10, 216, 20, WHITE)
    

    
    rl.DrawTexture(tiles[placeTile], 16, 150, WHITE)

    rl.EndDrawing()

for i in tiles:
    rl.UnloadTexture(i)

Leave a Reply

Your email address will not be published. Required fields are marked *