Sokoban - the code¶
Objectives¶
This part shows the code for a basic sokoban game implemented with Blender and Python. |
Instructions¶
Tasks: |
---|
- If you haven’t done it so far, download the
blend-file
- Read the code, improve it and try to understand it
- Add materials and textures to the objects
- Create own levels
- 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()