Sokoban - the code

Objectives

a This part shows the code for a basic sokoban game implemented with Blender and Python.

Instructions

Tasks:
  1. If you haven’t done it so far, download the blend-file
  2. Read the code, improve it and try to understand it
  3. Add materials and textures to the objects
  4. Create own levels
  5. Save highscores to a file

The blend-file

Blender has a System with 20 Layers, which can switched on and off. You can do that with the keyboard or with the GUI.

  • Keyboard:

    Change the layer: Press a (not numpad) number from 1 - 20

    Activate a layer: Press Shift + a (not numpad) number from 1 - 20

  • GUI:

    Change the layer: Klick on an layer

    Activate a layer: Shift + klick on a layer

Note: The layer of the selected object includes a yellow point. Active layers are darker than unactivated. If you want to change the layer of the selected object, click M + the layers number.

If you start the game, only the 1st layer is allowed to be active. The 2nd layer with your objects must be inactive or you’ll get an error.

The 1st layer

On the first layer are laying only 5 Objects:

  • Two cameras, one for the game view and one for displaying different screens
  • Two emptys, one for adding game objects and one for adding screens
  • One lamp

The 2nd layer

All the other objects, which are needed for the game, are laying on the 2nd layer and the programm will add them to the game:

  • player
  • static/moveable box
  • about, help and info screen
  • end point
  • ground

The Script

The Script is called every frame (that makes an always sensor which calls the script), so we can’t store global variables in our script. So we need an object which exists all the time to store our variables there. I used the »add_empty« to do this and applied the always sensor to it, too. So we can save variables like this:

        a["state"] = "intro"

»a« is our »add_empty« and we store our state, which says if the player is in the intro or playing a level. Now we are on the second point: We execute always the same script, but we want to do different actions - displaying the intro and playing levels. So we must save what we did last and where we want to continue. So we need states, in our case in named them »intro« and »play«. Each state has an own loop method, which is called when it’s active. If the state changes, another method is called and adds the object for the state. This all happens in the method main():

def main():
    scene = bge.logic.getCurrentScene()
    # we use the empty to store variables
    a = scene.objects["add_empty"]
    if "state" not in a:
        # first method call, set state to intro and setup intro scene
        a["state"] = "intro"
        a["level_index"] = 0
        add_intro_objects()
    if a["state"] == "intro":
        # intro is active
        get = intro_loop()
        if get == "end":
            bge.logic.endGame()
        elif get == "start":
            # play first level
            a["state"] = "play"
            create_level(data.levels[a["level_index"]])
            a["level_index"] += 1
    elif a["state"] == "play":
        get = play_loop()
        if get == "end":
            # go back to intro
            a["state"] = "intro"
            add_intro_objects()
            a["level_index"] = 0
        elif get == "next":
            if a["level_index"] > len(data.levels) - 1:
                # all levels are played, go back to intro
                a["state"] = "intro"
                add_intro_objects()
                a["level_index"] = 0
            else:
                # play next level
                create_level(data.levels[a["level_index"]])
                a["level_index"] += 1

The Splash Screen (intro state)

Before starting the game, the player will see a splash screen. There he can call help and about. First, we have to set the camera active:

    camera = scene.objects["screen_cam"]
    scene.active_camera = camera

On keyboard input can show help, about or start the game. Here we look if »h« was pressed and if true, we hide intro and about screen and display the help screen.

    elif keyboard.events[bge.events.HKEY] == k_events:
        # display the help
        scene.objects["help"].setVisible(True)
        scene.objects["intro"].setVisible(False)
        scene.objects["about"].setVisible(False)

Adding objects

For adding an object, you need an object in the scene which is active yet. In our case we have the empty »add_empty« for adding objects and »screen_empty« for adding screens. The new objects will be added on the empty’s position with it’s rotation and scale.

I’ll show you here some parts of the code, you can find the whole code at the end of the page. Before you setup a new scene, you have to clear the old one. That can be done with the following lines:

    for object in scene.objects:
        if object.name not in ['Camera', 'Lamp', 'screen_empty',
                               'add_empty', 'screen_cam']:
            object.endObject()

After that, we can add new objets, for example a static box:

                # the static box object
                static_box_obj = scene.objectsInactive["static_box"]
                # add static box to scene
                scene.addObject(static_box_obj, a)

The variable »a« is in this case an empty on the place where the object will be added.

The Game (play state)

After adding the objects and setting up the new camera, we convert the level in an easier format without end points. We do this because the end points could be overwritten if player or box is on it.

    # make copy that can be changed and store it
    a["field"] = [list(string.replace(".", " ").replace("*", "$").
                  replace("+", "@")) for string in level[:]]

We don’t need them because we check by the positions if a box is over an end point, for example to set other colors.

def update_box_color(objects):
    """Gives the box another color, if it is over an end point"""
    # get all end points by name
    end_points = [object for object in objects if
                  object.name.startswith("end_point")]
    # get all moveable boxes by name
    moveable_boxes = [object for object in objects if
                      object.name.startswith("moveable_box")]
    for box in moveable_boxes:
        for point in end_points:
            # check if positions are the same
            if (int(box.position[0]), int(box.position[1])) \
               == (int(point.position[0]), int(point.position[1])):
                # change box color (red, green, blue, alpha)
                box.color = (.281, .232, .106, 1)
                break
            else:
                box.color = (.9, .872, .134, 1)

For more details read the comments in the code.

#! bpy

import aud  # for audio
import bge
import data
import timeit
import time
import os
from random import randint
from random import choice
from math import radians  # from degrees to radians
from math import degrees  # from radians to degrees


def play_loop():
    """Creates a new player and controls the game"""
    # our scene
    scene = bge.logic.getCurrentScene()
    # the object with the variables
    a = scene.objects["add_empty"]
    keyboard = bge.logic.keyboard
    # set box colors
    update_box_color(scene.objects)
    player = scene.objects["player"]
    # change the scene
    # read keyboard input
    k_events = bge.logic.KX_INPUT_JUST_ACTIVATED
    # uparrow key pressed
    if keyboard.events[bge.events.UPARROWKEY] == k_events:
        # move player forwards if he can
        move = can_move_player(player, a["field"], scene)
        if move:
            # player can move
            if move == 1:
                # move only the player
                move_forwards_player(player, a["field"])
            else:
                # move the player and the moveable box in front of him
                box = get_front_box(a["field"], player, scene.objects, scene)
                move_forwards_player(player, a["field"], box)
                # set box colors
                update_box_color(scene.objects)
                # check, if level is won
                if is_level_won(a["level"], a["field"]):
                    # end level
                    print("Won")
                    # play next level
                    return "next"
    elif keyboard.events[bge.events.LEFTARROWKEY] == k_events:
        rotate_left(player)
    elif keyboard.events[bge.events.RIGHTARROWKEY] == k_events:
        rotate_right(player)
    if keyboard.events[bge.events.QKEY] == k_events:
        # end level, go back to intro
        return "end"


def create_level(level):
    """Add the objects for a sobokan level"""
    # our active scene
    scene = bge.logic.getCurrentScene()
    # clear scene
    for object in scene.objects:
        if object.name not in ['Camera', 'Lamp', 'screen_empty',
                               'add_empty', 'screen_cam']:
            object.endObject()
    # our empty for adding
    a = scene.objects["add_empty"]
    # store the level variable
    a["level"] = level
    # our columns
    cols = len(level[0])
    # our rows
    rows = len(level)
    # the names of our created objects

    # add ground
    ground_pos = (rows - 1, cols - 1, -1)
    # the ground object
    ground_obj = scene.objectsInactive["ground"]
    # setup ground's adding position
    a.worldPosition = ground_pos
    # add the ground object to the scene
    ground = scene.addObject(ground_obj, a)
    # scale the ground
    ground.localScale[0] = rows
    ground.localScale[1] = cols
    # setup the camera
    camera = scene.objects["Camera"]
    scene.active_camera = camera
    # add the othe objects
    for row in range(rows):
        for i in range(cols):
            coords = (row * 2, i * 2, 0)
            # change adding position to coords
            a.worldPosition = coords
            if level[row][i] == "#":
                # the static box object
                static_box_obj = scene.objectsInactive["static_box"]
                # add static box to scene
                scene.addObject(static_box_obj, a)
            elif level[row][i] == "@":
                # the player object
                player_obj = scene.objectsInactive["player"]
                # Add the player
                player = scene.addObject(player_obj, a)
                player.applyRotation((radians(90), 0, 0))
            elif level[row][i] == "$":
                # the moveable box object
                moveable_box_obj = scene.objectsInactive["moveable_box"]
                # add a moveable box
                scene.addObject(moveable_box_obj, a)
            elif level[row][i] == ".":
                # the end point object
                end_point_obj = scene.objectsInactive["end_point"]
                # set the adding position to ground height
                a.worldPosition[2] = -.99
                # Add an end point
                scene.addObject(end_point_obj, a)
            elif level[row][i] == "*":
                # moveable box and end point on one field
                # the moveable box object
                moveable_box_obj = scene.objectsInactive["moveable_box"]
                # add a moveable box
                scene.addObject(moveable_box_obj, a)

                # the end point object
                end_point_obj = scene.objectsInactive["end_point"]
                # set the adding position to ground height
                a.worldPosition[2] = -.99
                # Add an end point
                scene.addObject(end_point_obj, a)
            elif level[row][i] == "+":
                # player and end point on one field
                # the player object
                player_obj = scene.objectsInactive["player"]
                # Add the player
                player = scene.addObject(player_obj, a)
                player.applyRotation((radians(90), 0, 0))

                # the end point object
                end_point_obj = scene.objectsInactive["end_point"]
                # set the adding position to ground height
                a.worldPosition[2] = -.99
                # Add an end point
                scene.addObject(end_point_obj, a)
    # make copy that can be changed and store it
    a["field"] = [list(string.replace(".", " ").replace("*", "$").
                  replace("+", "@")) for string in level[:]]


def move_forwards_player(game_object, field, box=None):
    """Moves the object (and a box, if given) one step forwards"""
    if box is not None:
        # update field
        field[int(box.worldPosition[0] / 2)][int(box.worldPosition[1] / 2)]\
            = " "
        # move the box in the »3D-View«
        box.orientation = game_object.orientation
        move(box, 2)
    # update field
    field[int(game_object.worldPosition[0] / 2)][int(
        game_object.worldPosition[1] / 2)] = " "
    # move the player in the »3D-View«
    move(game_object, 2)
    # update »3D-View«
    bge.logic.NextFrame()
    # update field
    if box:
        field[int(box.worldPosition[0] / 2)][int(box.worldPosition[1] / 2)]\
            = "$"
    field[int(game_object.worldPosition[0] / 2)][int(
        game_object.worldPosition[1] / 2)] = "@"


def move(game_object, distance):
    """Moves the object"""
    # get the objects orientation and move to the right direction
    if int(degrees(game_object.orientation.to_euler().z)) % 360 == 0:
        game_object.position[1] -= distance
    elif int(degrees(game_object.orientation.to_euler().z)) % 360 == 90:
        game_object.position[0] += distance
    elif int(degrees(game_object.orientation.to_euler().z)) % 360 == 180:
        game_object.position[1] += distance
    else:
        game_object.position[0] -= distance


def rotate(game_object, degrees):
    """Rotates the object around z axis"""
    game_object.applyRotation((0, 0, radians(degrees)))


def rotate_left(game_object):
    """Rotates the object 90 degrees to the left"""
    rotate(game_object, 90)


def rotate_right(game_object):
    """Rotates the object 90 degrees to the right"""
    rotate(game_object, -90)


def can_move_player(game_object, field, scene):
    """Check, if the player can be moved"""
    def check_for_obstruction(object_forwards, object_d_forwards):
        """Check, if object_forwards/object_d_forwards is an obstruction"""
        # place in front of the player is free
        if object_forwards == " " or object_forwards == ".":
            return 1
        # an obstruction is in front of the player
        elif object_forwards == "#":
            return 0
        # a movable box is in front of the player
        else:
            # there is nothing behind the box
            if object_d_forwards == " " or object_d_forwards == ".":
                return 2
            # there is something behind the box
            return 0

    # players position in the field
    player_position = (int(game_object.worldPosition[0] / 2),
                       int(game_object.worldPosition[1] / 2))
    try:
        # the object's orientation
        if int(degrees(game_object.orientation.to_euler().z)) % 360 == 0:
            # the object in front of the player
            object_forwards = field[player_position[0]][player_position[1] - 1]
            # the object after the object in front of he player
            object_d_forwards = field[player_position[0]][player_position[1]
                                                          - 2]
            return check_for_obstruction(object_forwards, object_d_forwards)
        elif int(degrees(game_object.orientation.to_euler().z)) % 360 == 90:
            object_forwards = field[player_position[0] + 1][player_position[1]]
            object_d_forwards = field[player_position[0]
                                      + 2][player_position[1]]
            return check_for_obstruction(object_forwards, object_d_forwards)
        elif int(degrees(game_object.orientation.to_euler().z)) % 360 == 180:
            object_forwards = field[player_position[0]][player_position[1] + 1]
            object_d_forwards = field[player_position[0]
                                      ][player_position[1] + 2]
            return check_for_obstruction(object_forwards, object_d_forwards)
        else:
            object_forwards = field[player_position[0] - 1][player_position[1]]
            object_d_forwards = field[player_position[0]
                                      - 2][player_position[1]]
            return check_for_obstruction(object_forwards, object_d_forwards)
    except IndexError:
        # object_d_forwards not in field so object_forwards must be static cube
        return 0


def get_front_box(field, player, objects, scene):
    """Returns the Blender object of the box in front of the player"""
    # players position in the field
    player_position = (int(player.worldPosition[0] / 2),
                       int(player.worldPosition[1] / 2))
    box = None
    # get player's orientation and check for a box
    if int(degrees(player.orientation.to_euler().z)) % 360 == 0:
        object_forwards = (player_position[0], player_position[1] - 1)
    elif int(degrees(player.orientation.to_euler().z)) % 360 == 90:
        object_forwards = (player_position[0] + 1, player_position[1])
    elif int(degrees(player.orientation.to_euler().z)) % 360 == 180:
        object_forwards = (player_position[0], player_position[1] + 1)
    else:
        object_forwards = (player_position[0] - 1, player_position[1])
    box = get_object_from_position(object_forwards,
                                   [object for object in scene.objects if
                                    object.name.startswith("moveable_box")])
    return box


def get_object_from_position(position, objects):
    """Get the object on position *position*"""
    for object in objects:
        if (int(object.position[0] / 2),
                int(object.position[1] / 2)) == position:
            return object


def is_level_won(level, field):
    """Look, if all moveable boxes are on an end point"""
    cols = len(level[0])
    rows = len(level)
    for row in range(rows):
        for i in range(cols):
            # is moveable box on an end point
            if field[row][i] == "$" and not (level[row][i] == "." or
                                             level[row][i] == "+" or
                                             level[row][i] == "*"):
                return False
    return True


def update_box_color(objects):
    """Gives the box another color, if it is over an end point"""
    # get all end points by name
    end_points = [object for object in objects if
                  object.name.startswith("end_point")]
    # get all moveable boxes by name
    moveable_boxes = [object for object in objects if
                      object.name.startswith("moveable_box")]
    for box in moveable_boxes:
        for point in end_points:
            # check if positions are the same
            if (int(box.position[0]), int(box.position[1])) \
               == (int(point.position[0]), int(point.position[1])):
                # change box color (red, green, blue, alpha)
                box.color = (.281, .232, .106, 1)
                break
            else:
                box.color = (.9, .872, .134, 1)


def intro_loop():
    """The intro before the first level is starting"""
    # our active scene
    scene = bge.logic.getCurrentScene()
    keyboard = bge.logic.keyboard
    # look if a key was pressed
    k_events = bge.logic.KX_INPUT_JUST_ACTIVATED
    # uparrow key pressed
    if keyboard.events[bge.events.SPACEKEY] == k_events:
        # start level
        return "start"
    elif keyboard.events[bge.events.HKEY] == k_events:
        # display the help
        scene.objects["help"].setVisible(True)
        scene.objects["intro"].setVisible(False)
        scene.objects["about"].setVisible(False)
    elif keyboard.events[bge.events.AKEY] == k_events:
        # show the about on the screen
        scene.objects["help"].setVisible(False)
        scene.objects["intro"].setVisible(False)
        scene.objects["about"].setVisible(True)
    if keyboard.events[bge.events.QKEY] == k_events:
        # if intro screen is shown, quit program
        return "end"
    return "ok"


def add_intro_objects():
    """Adds the objects which are needed for the intro"""
    # the active scene
    scene = bge.logic.getCurrentScene()
    # clear scene
    for object in scene.objects:
        if object.name not in ['Camera', 'Lamp', 'screen_empty',
                               'add_empty', 'screen_cam']:
            object.endObject()
    # the add_empty for adding objects
    a = scene.objects["add_empty"]
    # the screen_empty for adding screens
    s = scene.objects["screen_empty"]
    # set active camera
    camera = scene.objects["screen_cam"]
    scene.active_camera = camera
    # the intro screen object
    intro_screen_obj = scene.objectsInactive["intro"]
    # add intro screen
    scene.addObject(intro_screen_obj, s)
    # the help screen object
    help_obj = scene.objectsInactive["help"]
    # add the help screen
    help = scene.addObject(help_obj, s)
    # hide help screen
    help.setVisible(False)
    # the about screen object
    about_obj = scene.objectsInactive["about"]
    # add the about screen
    about = scene.addObject(about_obj, s)
    # hide about screen
    about.setVisible(False)


def main():
    scene = bge.logic.getCurrentScene()
    # we use the empty to store variables
    a = scene.objects["add_empty"]
    if "state" not in a:
        # first method call, set state to intro and setup intro scene
        a["state"] = "intro"
        a["level_index"] = 0
        add_intro_objects()
    if a["state"] == "intro":
        # intro is active
        get = intro_loop()
        if get == "end":
            bge.logic.endGame()
        elif get == "start":
            # play first level
            a["state"] = "play"
            create_level(data.levels[a["level_index"]])
            a["level_index"] += 1
    elif a["state"] == "play":
        get = play_loop()
        if get == "end":
            # go back to intro
            a["state"] = "intro"
            add_intro_objects()
            a["level_index"] = 0
        elif get == "next":
            if a["level_index"] > len(data.levels) - 1:
                # all levels are played, go back to intro
                a["state"] = "intro"
                add_intro_objects()
                a["level_index"] = 0
            else:
                # play next level
                create_level(data.levels[a["level_index"]])
                a["level_index"] += 1


main()