• No results found

Building 3D maps and mazes

We've seen that the Pi3D library can be used to create lots of interesting objects and environments. Using some of the more complex classes (or by constructing our own), whole custom spaces can be designed for the user to explore.

In the following example, we use a special module called Building, which has been designed to allow you to construct a building using a single image file to provide the layout.

Getting ready

You will need to ensure that you have the following files in the pi3d/textures directory:

f squareblocksred.png f floor.png

f inside_map0.png, inside_map1.png, inside_map2.png

These files are available as part of the book resources placed at Chapter05\resource\ source_files\textures.

How to do it…

Let's run the following 3dMaze.py script by performing the following steps:

1. First, we set up the display and settings for the model using the following code: #!/usr/bin/python3

"""Small maze game, try to find the exit """

from math import sin, cos, radians import demo

import pi3d

from pi3d.shape.Building import Building, SolidObject from pi3d.shape.Building import Size, Position

# Set up display and initialize pi3d DISPLAY = pi3d.Display.create() #Load shader shader = pi3d.Shader("uv_reflect") flatsh = pi3d.Shader("uv_flat") # Load textures ceilingimg = pi3d.Texture("textures/squareblocks4.png") wallimg = pi3d.Texture("textures/squareblocksred.png") floorimg = pi3d.Texture("textures/dunes3_512.jpg") bumpimg = pi3d.Texture("textures/mudnormal.jpg") startimg = pi3d.Texture("textures/rock1.jpg") endimg = pi3d.Texture("textures/water.jpg") # Create elevation map

mapwidth=1000.0 mapdepth=1000.0

mymap = pi3d.ElevationMap(mapfile="textures/floor.png",

width=mapwidth, depth=mapdepth, height=mapheight, divx=64, divy=64) mymap.set_draw_details(shader,[floorimg, bumpimg],128.0, 0.0) levelList=["textures/inside_map0.png","textures/inside_map1.png", "textures/inside_map2.png"] avhgt = 5.0 aveyelevel = 4.0 MAP_BLOCK = 15.0

aveyeleveladjust = aveyelevel - avhgt/2

PLAYERHEIGHT=(mymap.calcHeight(5, 5) + avhgt/2) #Start the player in the top-left corner

startpos=[(8*MAP_BLOCK),PLAYERHEIGHT,(8*MAP_BLOCK)] endpos=[0,PLAYERHEIGHT,0] #Set the end pos in the centre person = SolidObject("person", Size(1, avhgt, 1),

Position(startpos[0],startpos[1],startpos[2]), 1) #Add spheres for start and end; the end must also have a solid #object so we can detect when we hit it

startobject=pi3d.Sphere(name="start",x=startpos[0],

y=startpos[1]+avhgt,z=startpos[2]) startobject.set_draw_details(shader, [startimg, bumpimg], 32.0, 0.3)

endobject=pi3d.Sphere(name="end",x=endpos[0], y=endpos[1],z=endpos[2])

endobject.set_draw_details(shader, [endimg, bumpimg], 32.0, 0.3) endSolid = SolidObject("end", Size(1, avhgt, 1),

Position(endpos[0],endpos[1],endpos[2]), 1) mazeScheme = {"#models": 3,

(1,None): [["C",2]], #white cell : Ceiling (0,1,"edge"): [["W",1]], #white cell on edge next # black cell : Wall (1,0,"edge"): [["W",1]], #black cell on edge next # to white cell : Wall (0,1):[["W",0]]} #white cell next

# to black cell : Wall details = [[shader, [wallimg], 1.0, 0.0, 4.0, 16.0], [shader, [wallimg], 1.0, 0.0, 4.0, 8.0], [shader, [ceilingimg], 1.0, 0.0, 4.0, 4.0]] arialFont = pi3d.Font("fonts/FreeMonoBoldOblique.ttf",

2. We then create functions to allow us to reload the levels and display messages to the player using the following code:

def loadLevel(next_level):

print(">>> Please wait while maze is constructed...") next_level=next_level%len(levelList)

building = pi3d.Building(levelList[next_level], 0, 0, mymap, width=MAP_BLOCK, depth=MAP_BLOCK, height=30.0,

name="", draw_details=details, yoff=-15, scheme=mazeScheme) return building

def showMessage(text,rot=0):

message = pi3d.String(font=arialFont, string=text, x=endpos[0],y=endpos[1]+(avhgt/4), z=endpos[2], sx=0.05, sy=0.05,ry=-rot) message.set_shader(flatsh)

message.draw()

3. Within the main function, we set up the 3D environment and draw all the objects using the following code:

def main(): #Load a level level=0 building = loadLevel(level) lights = pi3d.Light(lightpos=(10, -10, 20), lightcol =(0.7, 0.7, 0.7), lightamb=(0.7, 0.7, 0.7)) rot=0.0 tilt=0.0 CAMERA = pi3d.Camera.instance()

while DISPLAY.loop_running() and not \

inputs.key_state("KEY_ESC"): CAMERA.reset() CAMERA.rotate(tilt, rot, 0) CAMERA.position((person.x(), person.y(), person.z() - aveyeleveladjust)) #draw objects person.drawall() building.drawAll() mymap.draw() startobject.draw() endobject.draw()

b.set_light(lights, 0) mymap.set_light(lights, 0) inputs.do_input_events()

#Note:Some mouse devices will be located on #get_mouse_movement(1) or (2) etc. mx,my,mv,mh,md=inputs.get_mouse_movement() rot -= (mx)*0.2 tilt -= (my)*0.2 xm = person.x() ym = person.y() zm = person.z()

4. Finally, we monitor for key presses, handle any collisions with objects, and move within the maze as follows:

if inputs.key_state("KEY_APOSTROPHE"): #key ' tilt -= 2.0 if inputs.key_state("KEY_SLASH"): #key / tilt += 2.0 if inputs.key_state("KEY_A"): rot += 2 if inputs.key_state("KEY_D"): rot -= 2 if inputs.key_state("KEY_H"):

#Use point_at as help - will turn the player to face # the direction of the end point

tilt, rot = CAMERA.point_at([endobject.x(), endobject.y(), endobject.z()]) if inputs.key_state("KEY_W"): xm -= sin(radians(rot)) zm += cos(radians(rot)) if inputs.key_state("KEY_S"): xm += sin(radians(rot)) zm -= cos(radians(rot)) NewPos = Position(xm, ym, zm)

collisions = person.CollisionList(NewPos) if collisions:

#If we reach the end, reset to start position! for obj in collisions:

#Required to remove the building walls from the # solidobject list building.remove_walls() showMessage("Loading Level",rot) DISPLAY.loop_running() level+=1 building = loadLevel(level) showMessage("") person.move(Position(startpos[0],startpos[1], startpos[2])) else: person.move(NewPos) try: main() finally: inputs.release() DISPLAY.destroy()

print("Closed Everything. END") #End

How it works...

We define many of the elements we used in the preceding examples, such as the display, textures, shaders, font, and lighting. We also define the objects, such as the building itself, the ElevationMap object, as well as the start and end points of the maze. We also use

SolidObjects to help detect movement within the space. See the Using SolidObjects to detect

collisions subsection in the There's more… section of this recipe for more information. Finally, we create the actual Building object based on the selected map image (using the loadLevel() function) and locate the camera (which represents our first-person viewpoint) at the start. See the The Building module subsection in the There's more… section of this recipe for more information.

Within the main loop, we draw all the objects in our space and apply the lighting effects. We will also monitor the inputs events for movement in the mouse (to control the tilt and rotation of the camera) or the keyboard to move the player (or exit/provide help).

The controls are as follows:

f Mouse movement: This changes the camera tilt and rotation.

f W or S: This moves the player forwards or backwards.

f H: This helps the player by rotating them to face the end of the maze. The useful

CAMERA.point_at() function is used to quickly rotate and tilt the camera's viewpoint towards the provided coordinates (the end position).

Whenever the player moves, we check if the new position (NewPos) collides with another SolidObject using CollisionList(NewPos). The function will return a list of any other SolidObjects that overlap the coordinates provided.

If there are no SolidObjects in the way, we make the player move; otherwise, we check to see if one of the SolidObject's names is the end object, in which case we have reached the end of the maze.

When the player reaches the end, we clear the inputs, remove the walls from the old Building object, and display a loading message. If we forget to remove the walls, all the SolidObjects belonging to the previous Building will still remain, creating invisible obstacles in the next level.

We use the showMessage() function to inform the user that the next level will be loaded soon (since it can take a while for the building object to be constructed). We need to ensure that we call DISPLAY.loop_running() after we draw the message so that it is displayed on screen before we start loading the level (at which point the person will be unable to move and so on). We need to ensure that the message is always facing the player regardless of the side they collide with the end object, by using the camera rotation (rot) for its angle.

When the exit ball is found, the next level is loaded

When the next level in the list has been loaded (or the first level has been loaded again when all the levels have been completed), we replace the message with a blank one to remove it

You can design and add your own levels by creating additional map files (20 x 20 PNG files with walls marked out with black pixels and walkways in white) and listing them in levelList. The player will start at the top-left corner of the map, and the exit is placed at the center.

You will notice that loading the levels can take quite a long time; this is the relatively slow ARM processor in the Raspberry Pi performing all the calculations required to construct the maze and locate all the components. As soon as the maze has been built, the more powerful GPU takes over, which results in fast and smooth graphics as the player explores the space.