├── .gitignore ├── maze_2d_gen.py ├── maze_3D_demo.blend ├── maze_3d.py ├── maze__2D_simple.blend ├── maze__2D_simple_path.png ├── maze__2D_simple_wall.png ├── maze_any_mesh_3d_grid.blend ├── maze_mesh.py ├── maze_passage_3D.py ├── readme.md ├── red_maze_blue_maze.png ├── red_maze_blue_maze ├── .gitignore ├── AnimationHelper.py ├── Camera.py ├── Config.py ├── Controller.py ├── Gamepad_Input.py ├── GravPlanet.py ├── Gravswitch.py ├── Input.py ├── Instructions.txt ├── Notes.txt ├── Player.py ├── TMath.py ├── red_maze_blue_maze.blend └── sky_blue_02.png ├── steely_taws_2d_maze.blend └── steely_taws_screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.blend1 3 | -------------------------------------------------------------------------------- /maze_2d_gen.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sverchok scripted node for simple 2D maze generation 3 | elfnor.com 2015 4 | 5 | maze library by Sami Salkosuo 6 | https://gist.github.com/samisalkosuo/77bd95f605fc41dc7366 7 | 8 | following "Mazes for Programmers" 9 | https://pragprog.com/book/jbmaze/mazes-for-programmers 10 | """ 11 | import mathutils as mu 12 | from sverchok.data_structure import Matrix_listing 13 | 14 | import maze_3d 15 | 16 | from math import pi 17 | 18 | 19 | class SvGrid(maze_3d.Grid): 20 | 21 | def pathMatrices(self): 22 | """ 23 | outputs: list of mathutils Matrix mats 24 | list of integers mask 25 | 26 | mats: location and orientation of maze path tiles 27 | mask: which type of path tile to place 28 | """ 29 | mats = [] 30 | mask = [] 31 | for row in self.eachRow(): 32 | for cell in row: 33 | ngh_ID = cell.linkedID() 34 | tile_type, Rmat = tiles[ngh_ID] 35 | 36 | Tmat = mu.Matrix.Translation(mu.Vector((cell.row, cell.column, 0.0))) 37 | mats.append(Tmat * Rmat) 38 | mask.append(tile_type) 39 | 40 | return mats, mask 41 | 42 | 43 | I = mu.Matrix.Identity(4) 44 | R90 = mu.Matrix.Rotation(pi/2.0, 4, 'Z') 45 | R180 = mu.Matrix.Rotation(pi, 4, 'Z') 46 | R270 = mu.Matrix.Rotation(1.5*pi, 4, 'Z') 47 | 48 | # The tiles dictionary stores info about which tile to 49 | # place and what orientation to place it in based on the 50 | # neighbours of each path tile. 51 | # neighbours ID: (tile type, tile rotation matrix) 52 | # the neighbours ID can be found for a path tile by adding up the values 53 | # 2 54 | # 4#1 55 | # 8 56 | # for those neighbours that are linked 57 | 58 | # the tile types: 59 | # 0 : four way junction 60 | # 1 : three way junction 61 | # 2 : bend 62 | # 3 : straight 63 | # 4 : dead end 64 | 65 | tiles = {1: (4, R180), 66 | 2: (4, R90), 67 | 3: (2, R90), 68 | 4: (4, I), 69 | 5: (3, I), 70 | 6: (2, I), 71 | 7: (1, I), 72 | 8: (4, R270), 73 | 9: (2, R180), 74 | 10: (3, R90), 75 | 11: (1, R90), 76 | 12: (2, R270), 77 | 13: (1, R180), 78 | 14: (1, R270), 79 | 15: (0, I)} 80 | 81 | 82 | def sv_main(rseed=21, width=21, height=11, scale=1.0, braid = 0.0): 83 | 84 | in_sockets = [['s', 'rseed', rseed], 85 | ['s', 'width', width], 86 | ['s', 'height', height], 87 | ['s', 'scale', scale], 88 | ['s', 'braid', braid]] 89 | 90 | maze_3d.random.seed(rseed) 91 | grid=SvGrid(width, height) 92 | grid=maze_3d.initRecursiveBacktrackerMaze(grid) 93 | grid.braid(braid) 94 | 95 | print(grid) 96 | 97 | mats, mask = grid.pathMatrices() 98 | 99 | #scale locations 100 | for m in mats: 101 | for i in range(3): 102 | m[i][3] = m[i][3] * scale 103 | 104 | mat_out = Matrix_listing(mats) 105 | 106 | out_sockets = [ 107 | ['m', 'matrices', mat_out], 108 | ['s', 'mask', [mask]] 109 | ] 110 | 111 | return in_sockets, out_sockets 112 | -------------------------------------------------------------------------------- /maze_3D_demo.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/mazes/d93906825212980398a5895cb21ea321cf1e06ab/maze_3D_demo.blend -------------------------------------------------------------------------------- /maze_3d.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # started as gist from https://gist.github.com/samisalkosuo/77bd95f605fc41dc7366 4 | # adapted for use in Blender Sverchok 5 | # 3D diagonal classes added 6 | 7 | #Some mazes classes translated from Ruby 8 | #from book "Mazes for Programmers" by Jamis Buck. 9 | #https://pragprog.com/book/jbmaze/mazes-for-programmers 10 | 11 | import random 12 | 13 | class Cell: 14 | 15 | def __init__(self,row,column): 16 | self.row=row 17 | self.column=column 18 | self.neighborKeys = ('north', 'east', 'south', 'west') 19 | self.neighborDirns = dict(zip(self.neighborKeys, ((-1, 0), (0, 1), (1, 0), (0, -1)))) 20 | self.neighborDict = dict.fromkeys(self.neighborKeys) 21 | self.links=dict() 22 | 23 | def link(self,cell,bidi=True): 24 | self.links[cell] = True 25 | if bidi==True: 26 | cell.link(self,False) 27 | return self 28 | 29 | def unlink(self,cell,bidi=True): 30 | try: 31 | del self.links[cell] 32 | except KeyError: 33 | pass 34 | if bidi==True: 35 | cell.unlink(self, False) 36 | return self 37 | 38 | def getLinks(self): 39 | return self.links.keys() 40 | 41 | def linked(self,cell): 42 | return cell in self.links 43 | 44 | def neighbors(self): 45 | """ 46 | returns list of neighbors that exist 47 | """ 48 | neighborList = [cell for k, cell in self.neighborDict.items() if cell] 49 | return neighborList 50 | 51 | def getDistances(self): 52 | distances=Distances(self) 53 | frontier=[] 54 | frontier.append(self) 55 | while len(frontier)>0: 56 | newFrontier=[] 57 | for cell in frontier: 58 | for linked in cell.getLinks(): 59 | if distances.getDistanceTo(linked) is None: 60 | dist=distances.getDistanceTo(cell) 61 | distances.setDistanceTo(linked,dist+1) 62 | newFrontier.append(linked) 63 | frontier=newFrontier 64 | return distances 65 | 66 | def linkedID(self): 67 | """ 68 | returns an integer representing which neighbors are linked 69 | this is binary, self.neighborKeys[0] is LSB 70 | self.neighborKeys[-1] is MSB 71 | """ 72 | ngh = [self.linked(self.neighborDict[dirn]) for dirn in self.neighborKeys ] 73 | nghID = sum( 2**i*b for i, b in enumerate(ngh)) 74 | return nghID 75 | 76 | def __str__(self): 77 | nghID = self.linkedID() 78 | output="Cell[%d,%d], Linked neighbors ID:%d " % (self.row,self.column, nghID) 79 | return output 80 | 81 | class Distances: 82 | 83 | def __init__(self,rootCell): 84 | self.rootCell=rootCell 85 | self.cells=dict() 86 | self.cells[self.rootCell]=0 87 | 88 | def getDistanceTo(self,cell): 89 | return self.cells.get(cell,None) 90 | 91 | def setDistanceTo(self,cell,distance): 92 | self.cells[cell]=distance 93 | 94 | def getCells(self): 95 | return self.cells.keys() 96 | 97 | def isPartOfPath(self,cell): 98 | return self.cells.has_key(cell) 99 | 100 | def __len__(self): 101 | return len(self.cells.keys()) 102 | 103 | def pathTo(self,goal): 104 | current=goal 105 | breadcrumbs = Distances(self.rootCell) 106 | breadcrumbs.setDistanceTo(current,self.cells[current]) 107 | 108 | while current is not self.rootCell: 109 | for neighbor in current.getLinks(): 110 | if self.cells[neighbor] < self.cells[current]: 111 | breadcrumbs.setDistanceTo(neighbor,self.cells[neighbor]) 112 | current=neighbor 113 | break 114 | return breadcrumbs 115 | 116 | 117 | class Grid: 118 | 119 | def __init__(self,rows,columns,cellClass=Cell): 120 | self.CellClass=cellClass 121 | self.rows=rows 122 | self.columns=columns 123 | self.grid=self.prepareGrid() 124 | self.distances=None 125 | self.configureCells() 126 | 127 | def prepareGrid(self): 128 | rowList=[] 129 | for i in range(self.rows): 130 | columnList=[] 131 | for j in range(self.columns): 132 | columnList.append(self.CellClass(i,j)) 133 | rowList.append(columnList) 134 | return rowList 135 | 136 | def eachRow(self): 137 | for row in self.grid: 138 | yield row 139 | 140 | def eachCell(self): 141 | for row in self.grid: 142 | for cell in row: 143 | yield cell 144 | 145 | def configureCells(self): 146 | for cell in self.eachCell(): 147 | row=cell.row 148 | col=cell.column 149 | for dirn in cell.neighborDirns: 150 | cell.neighborDict[dirn] = self.getNeighbor(row + cell.neighborDirns[dirn][0], 151 | col + cell.neighborDirns[dirn][1]) 152 | 153 | def getCell(self,row,column): 154 | return self.grid[row][column] 155 | 156 | def getNeighbor(self,row,column): 157 | if not (0 <= row < self.rows): 158 | return None 159 | if not (0 <= column < self.columns): 160 | return None 161 | return self.grid[row][column] 162 | 163 | def size(self): 164 | return self.rows*self.columns 165 | 166 | def randomCell(self): 167 | row=random.randint(0, self.rows-1) 168 | column = random.randint(0, self.columns - 1) 169 | return self.grid[row][column] 170 | 171 | def contentsOf(self,cell): 172 | return " " 173 | 174 | def __str__(self): 175 | return self.asciiStr() 176 | 177 | def unicodeStr(self): 178 | pass 179 | 180 | def asciiStr(self): 181 | output = "+" + "---+" * self.columns + "\n" 182 | for row in self.eachRow(): 183 | top = "|" 184 | bottom = "+" 185 | for cell in row: 186 | if not cell: 187 | cell=Cell(-1,-1) 188 | body = "%s" % self.contentsOf(cell) 189 | if cell.linked(cell.neighborDict['east']): 190 | east_boundary=" " 191 | else: 192 | east_boundary="|" 193 | 194 | top = top+ body + east_boundary 195 | if cell.linked(cell.neighborDict['south']): 196 | south_boundary=" " 197 | else: 198 | south_boundary="---" 199 | corner = "+" 200 | bottom =bottom+ south_boundary+ corner 201 | 202 | output=output+top+"\n" 203 | output=output+bottom+"\n" 204 | return output 205 | 206 | def deadends(self): 207 | """ 208 | returns a list of maze deadends 209 | """ 210 | ends = [cell for cell in self.eachCell() if len(cell.links) == 1 ] 211 | return ends 212 | 213 | def braid(self, p=1.0): 214 | """ 215 | Add links between dead ends (only one neighbour) and a neighbouring cell 216 | p is the proportion (approx) of dead ends that are culled. Default p=1.0 removes 217 | them all. 218 | Linkind dead ends produces loops in the maze. 219 | Prefers to link to another dead end if possible 220 | """ 221 | 222 | random.shuffle(self.deadends()) 223 | for cell in self.deadends(): 224 | if (len(cell.links) == 1) and (random.random() < p): 225 | #its still a dead end, ignore some if p < 1 226 | # find neighbours not linked to cell 227 | unlinked = [ngh for ngh in cell.neighbors() if not(cell.linked(ngh))] 228 | #find unlinked neighbours that are also dead ends 229 | best = [ngh for ngh in unlinked if len(ngh.links) == 1] 230 | if len(best) == 0: 231 | best = unlinked 232 | ngh = random.choice(best) 233 | cell.link(ngh) 234 | 235 | class DistanceGrid(Grid): 236 | 237 | #def __init__(self,rows,columns,cellClass=Cell): 238 | # super(Grid, self).__init__(rows,columns,cellClass) 239 | 240 | def contentsOf(self,cell): 241 | 242 | if self.distances.getDistanceTo(cell) is not None and self.distances.getDistanceTo(cell) is not None: 243 | n=self.distances.getDistanceTo(cell) 244 | return "%03d" % n 245 | else: 246 | return " " #super(Grid, self).contentsOf(cell) 247 | 248 | ## 3D maze neighbours are defined as 249 | # 4 cells on same level, 250 | # 4 cells above 251 | # 4 cells below 252 | 253 | class Cell3dDiag(Cell): 254 | 255 | def __init__(self, level, row, column): 256 | self.level = level 257 | Cell.__init__(self, row, column) 258 | self.neighborKeys = ('north', 'east', 'south', 'west', 259 | 'northUp', 'eastUp', 'southUp', 'westUp', 260 | 'northDown', 'eastDown', 'southDown', 'westDown') 261 | self.neighborDirns = dict(zip(self.neighborKeys, 262 | ((0,-1, 0), (0, 0, 1), (0, 1, 0), (0, 0, -1), 263 | (1,-1, 0), (1, 0, 1), (1, 1, 0), (1, 0, -1), 264 | (-1,-1, 0), (-1, 0, 1), (-1, 1, 0), (-1, 0, -1) ))) 265 | 266 | self.neighborDict = dict.fromkeys(self.neighborKeys) 267 | 268 | def __str__(self): 269 | nghID = self.linkedID() 270 | output="Cell[%d, %d, %d], Linked neighbors ID:%d " % (self.level, self.row,self.column, nghID) 271 | return output 272 | 273 | class Grid3dDiag(Grid): 274 | 275 | def __init__(self, levels, rows, columns): 276 | self.levels = levels 277 | Grid.__init__(self, rows, columns, cellClass = Cell3dDiag) 278 | 279 | def prepareGrid(self): 280 | """ 281 | grid is a triple nested list of cells 282 | """ 283 | levelList=[] 284 | for h in range(self.levels): 285 | rowList = [] 286 | for i in range(self.rows): 287 | columnList=[] 288 | for j in range(self.columns): 289 | columnList.append(self.CellClass(h, i, j)) 290 | rowList.append(columnList) 291 | levelList.append(rowList) 292 | return levelList 293 | 294 | def eachLevel(self): 295 | for level in self,grid: 296 | yield level 297 | 298 | def eachRow(self): 299 | for level in self.grid: 300 | for row in level: 301 | yield row 302 | 303 | def eachCell(self): 304 | for level in self.grid: 305 | for row in level: 306 | for cell in row: 307 | yield cell 308 | 309 | def getCell(self,level, row, column): 310 | return self.grid[level][row][column] 311 | 312 | def getNeighbor(self,level, row, column): 313 | """ 314 | defines borders by returning None outside grid 315 | """ 316 | if not (0 <= level < self.levels): 317 | return None 318 | if not (0 <= row < self.rows): 319 | return None 320 | if not (0 <= column < self.columns): 321 | return None 322 | return self.grid[level][row][column] 323 | 324 | def configureCells(self): 325 | """ 326 | set up neighbours, defines edges 327 | """ 328 | for cell in self.eachCell(): 329 | level = cell.level 330 | row=cell.row 331 | col=cell.column 332 | for dirn in cell.neighborDirns: 333 | cell.neighborDict[dirn] = self.getNeighbor( 334 | level + cell.neighborDirns[dirn][0], 335 | row + cell.neighborDirns[dirn][1], 336 | col + cell.neighborDirns[dirn][2]) 337 | 338 | def size(self): 339 | return self.columns * self.rows * self.columns 340 | 341 | def randomCell(self): 342 | level = random.randint(0, self.levels - 1) 343 | row = random.randint(0, self.rows - 1) 344 | column = random.randint(0, self.columns - 1) 345 | return self.grid[level][row][column] 346 | 347 | def eachEdge(self): 348 | """ 349 | generator thst yields each potential path only once 350 | yields (cell, direction, type) 351 | where type 0 is a link, type is no link and type 3 352 | is on the edge of the maze, that is cell has no neighbor 353 | """ 354 | seen = set() 355 | for i, cell in enumerate(self.eachCell()): 356 | for dirn in cell.neighborDirns: 357 | if cell.neighborDict[dirn]: 358 | if cell.linked(cell.neighborDict[dirn]): 359 | typeID = 0 360 | else: 361 | typeID = 1 362 | celln = cell.neighborDict[dirn] 363 | neighbor_i = celln.level*self.rows*self.columns + celln.row*self.columns + celln.column 364 | ekey = [i, neighbor_i] 365 | ekey.sort() 366 | ekey = tuple(ekey) 367 | #print(seen) 368 | if ekey not in seen: 369 | seen.add(ekey) 370 | yield (cell, dirn, typeID) 371 | else: 372 | typeID = 2 373 | yield (cell, dirn, typeID) 374 | 375 | ## carving functions 376 | 377 | def initRecursiveBacktrackerMaze(grid): 378 | stack = [] 379 | stack.append(grid.randomCell()) 380 | 381 | while len(stack)>0: 382 | current = stack[-1] 383 | neighbors=[] 384 | for n in current.neighbors(): 385 | if len(n.getLinks())==0: 386 | neighbors.append(n) 387 | 388 | if len(neighbors)==0: 389 | stack.pop() 390 | else: 391 | neighbor = random.choice(neighbors) 392 | current.link(neighbor) 393 | stack.append(neighbor) 394 | 395 | return grid 396 | 397 | 398 | if __name__ == "__main__": 399 | 400 | grid=Grid3dDiag(4, 4, 4) 401 | #grid = Grid(10,10) 402 | grid=initRecursiveBacktrackerMaze(grid) 403 | 404 | print(grid.size()) 405 | 406 | #print(grid) 407 | 408 | 409 | -------------------------------------------------------------------------------- /maze__2D_simple.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/mazes/d93906825212980398a5895cb21ea321cf1e06ab/maze__2D_simple.blend -------------------------------------------------------------------------------- /maze__2D_simple_path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/mazes/d93906825212980398a5895cb21ea321cf1e06ab/maze__2D_simple_path.png -------------------------------------------------------------------------------- /maze__2D_simple_wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/mazes/d93906825212980398a5895cb21ea321cf1e06ab/maze__2D_simple_wall.png -------------------------------------------------------------------------------- /maze_any_mesh_3d_grid.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/mazes/d93906825212980398a5895cb21ea321cf1e06ab/maze_any_mesh_3d_grid.blend -------------------------------------------------------------------------------- /maze_mesh.py: -------------------------------------------------------------------------------- 1 | """ 2 | The cell and grid structure used in maze_3d.py is similar 3 | to the vertex and edge structure of a mesh. 4 | 5 | This sverchok scripted node produces a maze for any mesh input 6 | 7 | 8 | """ 9 | import random 10 | from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata, pydata_from_bmesh 11 | import bmesh 12 | import mathutils 13 | 14 | def edge_dict(n, edges): 15 | edge_dict = {k: [] for k in range(n)} 16 | for e in edges: 17 | edge_dict[e[0]].append(e[1]) 18 | edge_dict[e[1]].append(e[0]) 19 | return edge_dict 20 | 21 | def recursive_back_tracker_maze(verts, edges): 22 | """ 23 | verts: list of coordinates of vertices 24 | edges: list of pairs of vertex indices 25 | links: list of pairs of vertex connections that form a perfect maze 26 | """ 27 | stack = [] 28 | links = [] 29 | ed = edge_dict(len(verts), edges) 30 | ld = {k: [] for k in range(len(verts))} 31 | stack.append(random.randint(0, len(verts) - 1)) 32 | while len(stack) > 0: 33 | current = stack[-1] 34 | 35 | neighbors = [ 36 | n 37 | for n in ed[current] 38 | if len(ld[n]) == 0 39 | ] 40 | 41 | if len(neighbors)==0: 42 | stack.pop() 43 | else: 44 | neighbor = random.choice(neighbors) 45 | links.append([current, neighbor]) 46 | stack.append(neighbor) 47 | ld[current].append(neighbor) 48 | ld[neighbor].append(current) 49 | 50 | return links 51 | 52 | def do_braid(verts, edges, links, p=1.0): 53 | """ 54 | Add links between dead ends (only one neighbour) and a neighbouring vertex 55 | p is the proportion (approx) of dead ends that are culled. Default p=1.0 removes 56 | them all. 57 | Linking dead ends produces loops in the maze. 58 | Prefer to link to another dead end if possible 59 | """ 60 | ed = edge_dict(len(verts), edges) 61 | ld = edge_dict(len(verts), links) 62 | ends = [i for i,v in enumerate(verts) if len(ld[i]) == 1] 63 | random.shuffle(ends) 64 | braid_links = links[:] 65 | for v_id in ends: 66 | ngh_links = ld[v_id] 67 | if len(ngh_links) == 1 and (random.random() < p): 68 | #its still a dead end, ignore some if p < 1 69 | # find neighbours not linked to cell 70 | unlinked = [ngh for ngh in ed[v_id] if ngh not in ngh_links] 71 | 72 | #find unlinked neighbours that are also dead ends 73 | best = [ngh for ngh in unlinked if len(ld[ngh]) == 1] 74 | if len(best) == 0: 75 | best = unlinked 76 | ngh = random.choice(best) 77 | braid_links.append([v_id, ngh]) 78 | ld[v_id].append(ngh) 79 | ld[ngh].append(v_id) 80 | 81 | return braid_links 82 | 83 | def sv_main(verts=[],edges=[], faces=[], rseed=21, offset=20, braid = 0.0): 84 | 85 | in_sockets = [ 86 | ['v', 'Vertices', verts], 87 | ['s', 'Edges', edges], 88 | ['s', 'Faces', faces], 89 | ['s', 'rseed', rseed], 90 | ['s', 'offset', offset], 91 | ['s', 'braid', braid] 92 | ] 93 | links = [] 94 | verts_maze = [] 95 | verts_path = [] 96 | edges_path = [] 97 | faces_path = [] 98 | verts_wall = [] 99 | edges_wall = [] 100 | faces_wall = [] 101 | 102 | random.seed(rseed) 103 | 104 | if verts and edges: 105 | 106 | if faces: 107 | # don't use edges with a vertex on boundary for maze generation 108 | bm_maze = bmesh_from_pydata(verts[0], edges[0], faces[0]) 109 | # make a list of boundary verts 110 | boundary_verts = [v for v in bm_maze.verts if v.is_boundary] 111 | #remove these verts 112 | bmesh.ops.delete(bm_maze, geom=boundary_verts, context=1) 113 | #convert back to sverchok lists and generate maze on these 114 | verts_maze, edges_maze, faces_maze = pydata_from_bmesh(bm_maze) 115 | links = recursive_back_tracker_maze(verts_maze, edges_maze) 116 | if braid > 0 and braid <= 1.0: 117 | links = do_braid(verts_maze, edges_maze, links, braid) 118 | 119 | bm_maze.free() 120 | 121 | #bevel the whole mesh 122 | bm = bmesh_from_pydata(verts[0], edges[0], faces[0]) 123 | geom = list(bm.verts) + list(bm.edges) + list(bm.faces) 124 | 125 | bevel_faces = bmesh.ops.bevel(bm, geom=geom, offset=offset, 126 | offset_type=3, segments=1, 127 | profile=0.5, vertex_only=0, 128 | material=-1)['faces'] 129 | 130 | #match ids of bevelled face to links 131 | #find center of each new face in bevel mesh 132 | face_centers = [f.calc_center_median() for f in bm.faces] 133 | #make a kdtree from face centers of bevel faces 134 | kd = mathutils.kdtree.KDTree(len(face_centers)) 135 | for i, c in enumerate(face_centers): 136 | kd.insert(c, i) 137 | kd.balance() 138 | 139 | #find center of each link in maze 140 | link_centers = [] 141 | for e in links: 142 | x,y,z = zip(*[verts_maze[poi] for poi in e]) 143 | x,y,z = sum(x)/len(x), sum(y)/len(y), sum(z)/len(z) 144 | link_centers.append((x,y,z)) 145 | 146 | #find index of closest face center to the center of each link and each maze vertex 147 | path_face_ids = [kd.find(v)[1] for v in verts_maze + link_centers] 148 | 149 | 150 | #delete the walls form the path mesh 151 | bm_path = bm.copy() 152 | bm_path.faces.ensure_lookup_table() 153 | wall_faces = [ 154 | bm_path.faces[id] 155 | for id,fc in enumerate(face_centers) 156 | if id not in path_face_ids 157 | ] 158 | bmesh.ops.delete(bm_path, geom=wall_faces, context=5) 159 | 160 | verts_path, edges_path, faces_path = pydata_from_bmesh(bm_path) 161 | bm_path.free() 162 | 163 | #delete the path from the wall mesh 164 | bm.faces.ensure_lookup_table() 165 | path_faces = list(set([bm.faces[id] for id in path_face_ids])) 166 | bmesh.ops.delete(bm, geom=path_faces, context=5) 167 | verts_wall, edges_wall, faces_wall = pydata_from_bmesh(bm) 168 | bm.free() 169 | 170 | else: 171 | # no faces just make links 172 | verts_maze = verts[0] 173 | links = recursive_back_tracker_maze(verts[0], edges[0]) 174 | 175 | out_sockets = [ 176 | ['v', 'Link Vertices', [verts_maze] ], 177 | ['s', 'Link Edges', [links]], 178 | ['v', 'Path Vertices', [verts_path]], 179 | ['s', 'Path Edges', [edges_path] ], 180 | ['s', 'Path Faces', [faces_path] ], 181 | ['v', 'Wall Vertices', [verts_wall]], 182 | ['s', 'Wall Edges', [edges_wall] ], 183 | ['s', 'Wall Faces', [faces_wall] ] 184 | ] 185 | 186 | return in_sockets, out_sockets 187 | -------------------------------------------------------------------------------- /maze_passage_3D.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sverchok scripted node for simple 2D maze generation 3 | elfnor.com 2015 4 | 5 | maze library by Sami Salkosuo 6 | https://gist.github.com/samisalkosuo/77bd95f605fc41dc7366 7 | 8 | following "Mazes for Programmers" 9 | https://pragprog.com/book/jbmaze/mazes-for-programmers 10 | """ 11 | import mathutils as mu 12 | from sverchok.data_structure import Matrix_listing 13 | 14 | import maze_3d 15 | 16 | from math import pi 17 | 18 | pathDict = {'north': (pi, 5, 0.0, 1), 19 | 'east': (pi/2.0, 5, pi/2.0, 1), 20 | 'south': (0.0, 5, 0.0, 1), 21 | 'west' : (3.0*pi/2.0, 5, 3.0 * pi/2.0, 1), 22 | 'northUp': (pi, 6, pi, 2), 23 | 'eastUp' : (pi/2.0, 6, pi/2.0, 2), 24 | 'southUp': (0.0, 6, 0.0, 2), 25 | 'westUp': (3.0*pi/2.0, 6, 3.0*pi/2.0, 2), 26 | 'northDown': (pi, 7, 0.0, 2), 27 | 'eastDown': (pi/2.0, 7, 3.0*pi/2.0, 2 ), 28 | 'southDown': (0.0, 7, pi, 2), 29 | 'westDown' : (3.0*pi/2.0, 7, pi/2.0, 2 )} 30 | 31 | class SvGrid(maze_3d.Grid3dDiag): 32 | 33 | def pathMatrices(self): 34 | """ 35 | outputs: list of mathutils Matrix mats 36 | list of integers mask 37 | 38 | mats: location and orientation of maze path tiles 39 | mask: which type of path tile to place 40 | """ 41 | mats = [] 42 | mask = [] 43 | # platform matrices 44 | for cell in self.eachCell(): 45 | m = mu.Matrix.Identity(4) 46 | m[0][3] = cell.row 47 | m[1][3] = cell.column 48 | m[2][3] = cell.level 49 | mats.append(m) 50 | mask.append(0) 51 | 52 | # links matrices 53 | for cell, dirn, typeID in self.eachEdge(): 54 | # need a lookup table for all combinations of dirn and type ID 55 | if typeID == 2: 56 | #edge 57 | zrot = pathDict[dirn][0] 58 | m = mu.Matrix.Rotation(zrot, 4, 'Z') 59 | m[0][3] = cell.row 60 | m[1][3] = cell.column 61 | m[2][3] = cell.level 62 | mats.append(m) 63 | mask.append(pathDict[dirn][1]) 64 | else: 65 | #internal path or gap 66 | zrot = pathDict[dirn][2] 67 | m = mu.Matrix.Rotation(zrot, 4, 'Z') 68 | m[0][3] = cell.row + 0.5 * cell.neighborDirns[dirn][1] 69 | m[1][3] = cell.column + 0.5 * cell.neighborDirns[dirn][2] 70 | m[2][3] = cell.level + 0.5 * cell.neighborDirns[dirn][0] 71 | mats.append(m) 72 | if typeID == 0: 73 | mask.append(pathDict[dirn][3]) 74 | else: 75 | mask.append(pathDict[dirn][3] + 2) 76 | 77 | 78 | return mats, mask 79 | 80 | def sv_main(rseed=21, sizeX=4, sizeY=4, sizeZ=4, scaleXY=1.0, scaleZ=1.0, braid = 0.0): 81 | 82 | in_sockets = [['s', 'rseed', rseed], 83 | ['s', 'size X', sizeX], 84 | ['s', 'size Y', sizeY], 85 | ['s', 'size Z', sizeZ], 86 | ['s', 'scale XY', scaleXY], 87 | ['s', 'scale Z', scaleZ], 88 | ['s', 'braid', braid]] 89 | 90 | maze_3d.random.seed(rseed) 91 | grid=SvGrid(sizeZ, sizeX, sizeY) 92 | grid=maze_3d.initRecursiveBacktrackerMaze(grid) 93 | grid.braid(braid) 94 | 95 | #print(grid) 96 | 97 | mats, mask = grid.pathMatrices() 98 | 99 | #scale locations 100 | for m in mats: 101 | for i in range(2): 102 | m[i][3] = m[i][3] * scaleXY 103 | m[2][3] = m[2][3] * scaleZ 104 | 105 | mat_out = Matrix_listing(mats) 106 | 107 | out_sockets = [ 108 | ['m', 'matrices', mat_out], 109 | ['s', 'mask', [mask]] 110 | ] 111 | 112 | return in_sockets, out_sockets 113 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Mazes 2 | Sverchok Blender code for 2D and 3D maze generation 3 | 4 | ![path maze](maze__2D_simple_path.png) 5 | 6 | ![wall maze](maze__2D_simple_wall.png) 7 | 8 | ![steely taws](steely_taws_screenshot.png) 9 | 10 | ![red maze blue maze](red_maze_blue_maze.png) 11 | 12 | References 13 | * [elfnor's blog posts](http://elfnor.com/blender-2d-maze-generator.html) 14 | * [Jamis Buck's blog](http://weblog.jamisbuck.org/2011/2/7/maze-generation-algorithm-recap) and book "Mazes for Programmers". 15 | * [Sami Salkosuo's gist](https://gist.github.com/samisalkosuo/77bd95f605fc41dc7366) 16 | -------------------------------------------------------------------------------- /red_maze_blue_maze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/mazes/d93906825212980398a5895cb21ea321cf1e06ab/red_maze_blue_maze.png -------------------------------------------------------------------------------- /red_maze_blue_maze/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *~ 3 | *.blend1 4 | -------------------------------------------------------------------------------- /red_maze_blue_maze/AnimationHelper.py: -------------------------------------------------------------------------------- 1 | #I created the AnimationHelper.py module to help with the playing of animations. The biggest benefit of 2 | #this is that it allows for variable playing speed. It is intended to completely replace logic bricks and 3 | #object.playAction for playing actions, and should not be used in tandem with them for a single armature. 4 | #(Although it uses playAction internally) 5 | 6 | #The two main functions of this module are setLayerAnim and manageAnims. setLayerAnim is used to set what 7 | #animation should be playing on a specified layer for a specified armature. This information is stored as 8 | #properties in the target armature. manageAnims MUST be called EVERY FRAME for each armature that you 9 | #wish to be managed by AnimationHelper. It performs the necessary managing of actions, regulating play 10 | #speed, looping, etc. If you don't call it every frame, the results will be incorrect. 11 | 12 | import bge 13 | logic = bge.logic 14 | 15 | 16 | ###The Animation Class### 17 | 18 | #This is a class that stores animation properties, as well as the aname of an action. It has no methods, 19 | #it's essentially a bag of properties. The functions in this module (as well as the animation settings 20 | #in the Config.py module) use this class to represent animation data. 21 | #An Animation object is expected to contain the following properties(if it doesn't, manageAnims will fail): 22 | #'name', 'playType', 'playSpeed', 'blendin', 'entryFrame', 'endFrame' 23 | #However, 'playType', 'playSpeed', 'blendin', and 'entryFrame' are initialized to default values, so 24 | #you don't have to explicitly set them unless you want something other than the default. 25 | 26 | #Accepted values for playType are 'play', 'loop', and 'flip' (you should know what these mean if you're familiar 27 | #with BGE's animation system) 28 | 29 | #Important Note: 30 | #The Frame of Entry is NOT like the start_frame parameter of the object.playAction function, because start_frame 31 | #dictates one of the looping and flip flop points, whereas frame of entry merely influences the starting point 32 | #(the looping and flip flop points are always taken to be zero and LastFrame) 33 | class Animation: 34 | def __init__(self): 35 | #Default values 36 | self.playType = 'play' 37 | self.playSpeed = 1 38 | self.blendin = 10 39 | self.entryFrame = 0 40 | 41 | 42 | #This function takes an armature and layer, and Animation object, and sets the Animation to play on the 43 | #given layer for the given armature. It does this by storing the info as a property in the target armature. 44 | #manageAnims reads back this info and uses it to play the Animations for the armature. 45 | def setLayerAnim(armature, layer, animation): 46 | arm = armature 47 | if not 'Ready' in arm: 48 | arm['Ready'] = True 49 | arm['Old'] = [] 50 | arm['FlipDir1'] = [] 51 | arm['Layers'] = [] 52 | 53 | while len(arm['Old']) <= layer: 54 | arm['Old'].append(None) 55 | arm['FlipDir1'].append(False) 56 | arm['Layers'].append(None) 57 | 58 | arm['Layers'][layer] = animation 59 | 60 | #Given an armature, manageAnims will search it for animation info set by setLayerAnim, and will play and 61 | #manage actions for the given armature, based on the animation info. Must be called every frame for each 62 | #armature for proper behavior. 63 | def manageAnims(arm): 64 | anims = arm['Layers'] 65 | 66 | for index in range(len(anims)): 67 | anim = anims[index] 68 | 69 | if anim == None: 70 | arm.stopAction(index) 71 | 72 | else: 73 | old = arm['Old'][index] 74 | flipDir = arm['FlipDir1'][index] 75 | 76 | name = anim.name 77 | playType = anim.playType 78 | speed = anim.playSpeed 79 | endFrame = anim.endFrame 80 | blendin = anim.blendin 81 | entryFrame = anim.entryFrame 82 | 83 | if name != old: 84 | 85 | arm.playAction(name, 0, endFrame+1, index, 0, blendin, logic.KX_ACTION_MODE_PLAY) 86 | arm.setActionFrame(entryFrame, index) 87 | old = name 88 | flipDir = False 89 | 90 | if name == old: 91 | if flipDir == False: 92 | frame = arm.getActionFrame(index) + speed 93 | else: 94 | frame = arm.getActionFrame(index) - speed 95 | if playType == 'flip': 96 | if frame > endFrame: 97 | flipDir = True 98 | frame = endFrame 99 | if frame < 0: 100 | flipDir = False 101 | frame = 0 102 | if playType == 'loop' and frame >= endFrame: 103 | frame -= endFrame 104 | arm.setActionFrame(frame, index) 105 | 106 | arm['FlipDir1'][index] = flipDir 107 | 108 | arm['Old'][index] = old 109 | 110 | def getLayerAnim(arm, layer): 111 | try: return arm['Layers'][layer] 112 | except (KeyError, IndexError): return None 113 | 114 | -------------------------------------------------------------------------------- /red_maze_blue_maze/Camera.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | import bge 3 | import math 4 | import mathutils 5 | import TMath 6 | import Input 7 | import Config as C 8 | logic = bge.logic 9 | 10 | cont = logic.getCurrentController() 11 | own = cont.owner 12 | sce = logic.getCurrentScene() 13 | 14 | #get the camera's target player object 15 | player = sce.objects[C.Follow] 16 | #get the camera's and player's orientations 17 | pOrient = player.worldOrientation 18 | cOrient = own.worldOrientation 19 | 20 | #get the input vector for manual camera control 21 | input = Input.camInput() 22 | inputVec = input[0] 23 | #this variable tells the script whether to use auto or manual mode 24 | manual = (not input[1]) 25 | 26 | #create a manual angle variable if there is none (to store the manually adjusted vertical angle of cam) 27 | #Initialize it to the value of the 'Angle' property 28 | if not 'ManualAngle' in own: 29 | own['ManualAngle'] = C.Angle 30 | 31 | mAngle = own['ManualAngle'] 32 | prevAngle = mAngle 33 | 34 | #Toggle between auto and manual mode 35 | if not manual: 36 | #set manual angle equal to 'Angle' property in auto mode(in which case it isn't really manual) 37 | mAngle = C.Angle 38 | 39 | #player adjustment of the vertical angle in manual mode 40 | if manual == True: 41 | #modify the manual angle based on the vertical axis of the camera input vector 42 | mAngle += C.ManualTurnSpeed * inputVec[1] 43 | 44 | #Clamp the angle to within the defined range, to stop the user from doing crazy camera inversions. 45 | if mAngle > C.AngleLimitUp: 46 | mAngle = C.AngleLimitUp 47 | if mAngle < C.AngleLimitDown: 48 | mAngle = C.AngleLimitDown 49 | 50 | #This stores the difference between the current and previous vertical angles (to be used later) 51 | #The .9 multiplier just makes the camera's movements a bit more stable 52 | differenceAngle = .9 * (mAngle - prevAngle) 53 | 54 | #Store the manual angle in the 'ManualAngle' property 55 | own['ManualAngle'] = mAngle 56 | 57 | 58 | ###Setting The Cam's Orientation### 59 | 60 | #Rotate player orientation by 90 degrees (around X) plus the manual angle to create a target orientation 61 | #that is later interpolated with the current oreintation 62 | rotmat = mathutils.Matrix.Rotation(math.pi*0.5 - mAngle, 3, 'X') 63 | #target orientation 64 | tOrient = pOrient * rotmat 65 | 66 | if manual == True: 67 | #When manual control is active, this stops the camera from matching the player's z-axis rotation. 68 | #This means the cam won't automatically follow behind-the-back anymore. 69 | #However, the cool thing is that, if the player actually tilts sideways (due to gravity-bending madness), 70 | #or if the camera gets thrown off-kilter, it will right itself to be rightside-up relative to the player. 71 | #This is kind of hackish. It'd be better to only follow the x and y-axis rotations in the first place (but hard and tedious to code!) 72 | tVec = TMath.VecToPlane(tOrient.col[2],pOrient.col[0],pOrient.col[1]) 73 | cVec = TMath.VecToPlane(cOrient.col[2],pOrient.col[0],pOrient.col[1]) 74 | rotmat2 = TMath.VecToVecMatrix(cVec,tVec) 75 | 76 | tOrient = rotmat2 * tOrient 77 | 78 | #Generate turn speed factor to stop turning when player is facing towards camera, among other things 79 | #The factor is just 1(meaning no modification to the turn speed) when manual mode is on 80 | Fac = 1 81 | #when manual mode is off, the factor is determined like so 82 | if manual == False: 83 | CamProj = TMath.VecToPlane(cOrient.col[2], pOrient.col[0], pOrient.col[1]) 84 | TargetProj = TMath.VecToPlane(tOrient.col[2], pOrient.col[0], pOrient.col[1]) 85 | 86 | Fac = CamProj.dot(TargetProj) 87 | if Fac >= 0: 88 | Fac = 1 89 | else: 90 | Fac += 1 91 | if Fac < 0: 92 | Fac = 0 93 | Fac *= 8 94 | if Fac > 1: 95 | Fac = 1 96 | 97 | ##Interpolate between old and new cam orientation## 98 | 99 | #In manual mode... 100 | if manual == True: 101 | 102 | #Rotate the camera's orientation by the vertical difference angle from earlier. 103 | #Although the target orientation has already had the manual vertical angle applied to it, 104 | #and the camera's orientation will slowly adopt that angle through interpolation, 105 | #this speeds up the process so you don't have to wait for the camera's orientation to catch up 106 | #with the target orientation, therefore there will be no lag in manually adjusting the 107 | #camera's vertical angle. 108 | verticalAngleRotmat = mathutils.Matrix.Rotation(-differenceAngle,3,tOrient.col[0]) 109 | cOrient = verticalAngleRotmat * cOrient 110 | 111 | #Generate the matrix for the manual horizontal rotation 112 | manualHorizontalRotmat = mathutils.Matrix.Rotation(C.ManualTurnSpeed * inputVec[0], 3, pOrient.col[2]) 113 | 114 | #Perform the interpolation between the camera's current orientation and the target orientation (faster than in auto mode) 115 | cOrient = TMath.MatrixLerp(cOrient, tOrient, 1 - .97*(1 - C.TurnSpeed*Fac)) 116 | 117 | #Apply the manual horizontal rotation 118 | cOrient = manualHorizontalRotmat * cOrient 119 | 120 | #In auto mode, just do the interpolation step 121 | else: 122 | cOrient = TMath.MatrixLerp(cOrient, tOrient, C.TurnSpeed * Fac) 123 | 124 | 125 | ###Set the camera's distance, raycast to check for obstacles### 126 | 127 | #If there is no current distance variable, make one, set it to 'Distance' property(which is the max distance) 128 | if not 'CurrentDist' in own: 129 | own['CurrentDist'] = C.Distance 130 | 131 | pos = player.worldPosition 132 | pos2 = own.worldPosition 133 | ray = own.rayCast(pos2, pos, C.Distance, 'CamBlock', 0, 1) #Do raycast 134 | 135 | dist = own['CurrentDist'] 136 | 137 | #If the raycast hit something... 138 | if ray[0] != None: 139 | #If the hit distance is closer than the current distance, zoom in to the hit distance 140 | if(player.worldPosition-ray[1]).length < dist: 141 | dist = (player.worldPosition-ray[1]).length 142 | #Otherwise, blend between the current distance and hit distance (to zoom out gradually) 143 | else: 144 | dist = 0.95*dist+0.05*(player.worldPosition-ray[1]).length 145 | #Otherwise, blend between the current distance and the 'Distance' property 146 | else: 147 | dist = 0.95*dist+0.05*C.Distance 148 | 149 | own['CurrentDist'] = dist 150 | 151 | 152 | ###Computing The Cam's Position### 153 | 154 | #This section computes the cam's new position, with an added delay(using a "delay list") 155 | #The positional delay makes the camera look ahead of the player when he turns 156 | 157 | #create offset list, if it doesn't already exist 158 | if not 'CamOffsList' in own: 159 | own['CamOffsList'] = [] 160 | 161 | own['CamOffsList'].append(cOrient.col[2]) #add new offset vector to end of list(based on the current cam orientation) 162 | while len(own['CamOffsList']) > (C.LookAheadAmt /C.LookAheadSpd) + 1: #if list gets too long... 163 | own['CamOffsList'].pop(0) #remove the first element, push everything up to take it's place 164 | 165 | #If manual mode is on, and the offset list is longer than 1, shrink the offset list by two elements (one if there's only one). 166 | #This disables the offset during manual mode, but does so gradually 167 | if manual == True and len(own['CamOffsList']) > 1: 168 | own['CamOffsList'].pop(0) 169 | if manual == True and len(own['CamOffsList']) > 1: 170 | own['CamOffsList'].pop(0) 171 | 172 | #Get the first(oldest) offset vector stored in list, blend it with the newest offset vector 173 | CamOffs = own['CamOffsList'][0]*C.LookAheadSpd + cOrient.col[2]*(1-C.LookAheadSpd) 174 | #Normalize this new offset vector 175 | CamOffs.normalize() 176 | #Multiply the offset vector with the distance variable and add it to the player's location to get the camera's location 177 | cPos = player.worldPosition + CamOffs * dist 178 | cPos[2] += C.VerticalOffset #Add the vertical offset to the location 179 | 180 | 181 | #Set the final orientation and position 182 | own.worldOrientation = cOrient 183 | own.worldPosition = cPos -------------------------------------------------------------------------------- /red_maze_blue_maze/Config.py: -------------------------------------------------------------------------------- 1 | #####Config File##### 2 | #All values should be 0 or greater, unless otherwise specified 3 | 4 | #Any reference to 'Player' or 'Player Object' should be taken to mean the player's physics mesh, 5 | #not his display model. 6 | 7 | #BU = Blender Units 8 | 9 | import math 10 | from AnimationHelper import Animation 11 | 12 | ###Player settings### 13 | 14 | Cam = 'Camera' #Name of the camera that the player will move relative to 15 | 16 | Height = 1.5 #How high to position the player object's center above the ground (in BU) 17 | 18 | ResetSceneUponDeath = False 19 | 20 | ###Camera settings### 21 | 22 | Follow = 'Player' #Name of the object for the camera to follow(should be the player object) 23 | Angle = .5 #Elevation angle of the camera, in radians 24 | Distance = 8.0 #Distance from the player, in BU 25 | TurnSpeed = 0.015 #How fast the camera turns in auto mode (value between 0 and 1) 26 | LookAheadAmt = 13.0 #Amount to look ahead of the player when turning in auto mode 27 | LookAheadSpd = 0.2 #How suddenly to look ahead (auto mode only) 28 | VerticalOffset = 0.0 #Vertical offset of the camera, to make it higher or lower (in BU) 29 | ManualTurnSpeed = 0.035 #Turn speed with the manual controls 30 | AngleLimitUp = 1.0 #Highest angle of elevation the user can move the camera 31 | #Should be greater than AngleLimitDown, but not more than 1.5 32 | AngleLimitDown = -0.3 #Lowest angle of elevation the user can move the camera (Can be negative) 33 | #Should be less than AngleLimitUp, but not less than -1.5 34 | 35 | ###Control Input Settings 36 | 37 | mouseSensitivity = 37.5 38 | mouseSmooth = 0.3 39 | 40 | joystickThreshold = 0.25 41 | 42 | #On-land movement settings 43 | TurnSmoothness = 0.75 44 | WalkSpeed = 3.5 #Speed while not holding the run button 45 | MaxSpeed = 8.0 #Speed while running 46 | 47 | #Acceleration and Deceleration must be greater than 0 and no greater than 1. 48 | # 1.0 means instant accel/decel, 0.00001 means extremely slow accel/decel. 49 | Deceleration = 0.1 50 | Acceleration = 0.05 51 | 52 | #Death 53 | DeathDelay = 2 54 | 55 | #Spawn 56 | SpawnObjectName = 'SpawnHere' 57 | 58 | #Jump and gravity settings 59 | Jump = 8.0 #Jump strength 60 | ShortJump = .5 #Jump hieght multiplier when jump button is tapped, not held 61 | #(should be between 1 and 0, 1 for constant jump height) 62 | Gravity = 0.5 #Gravity strength (the player object ignores global gravity) 63 | TerminalVelocity = 50.0 #Maximum fall speed 64 | 65 | #Air control settings 66 | AirAccelFront = 0.25 #The rate of forward acceleration (due to directional input) in the air 67 | AirAccelBack = 0.25 68 | AirAccelSide = 0.2 69 | AirMaxFront = 8.0 #The maximum forward speed you can attain in the air 70 | #(If you start with a higher speed, i.e. from a running jump, you will keep your speed) 71 | AirMaxBack = 6.0 72 | AirMaxSide = 6.0 73 | 74 | #Stair and step settings 75 | Step = 0.6 #The maximum height (in BU) that can be stepped down from without detaching 76 | #from the ground. Should be less than Height. 77 | StepSmoothing = .75 #Amount of smoothing when the player climbs up or down a step 78 | #This setting impacts how steep your staircases can be without the player 79 | #detaching from them upon walking down them. See note #2 for details 80 | 81 | #Slope settings 82 | UphillSlopeFac = 1.0 #How much the player is slowed down when moving uphill. (0 for no effect) 83 | DownhillSlopeFac = 1.0 #How much the player is sped up when moving downhill. (0 for no effect) 84 | #(Can be greater than 1) 85 | 86 | ###Ledge hang settings 87 | 88 | UseLedgeHang = True #Enable or disable ledge hang 89 | 90 | MaxRayDistance = 0.9 #Maximum distance from player's center from which a ledge can be grabbed. 91 | MinRayDistance = 0.6 #Minimum distance from player's center from which a ledge can be grabbed. 92 | #Should be about equal to the distance from player center to furthest forward part of collision mesh 93 | 94 | NumRays = 4 #Increasing this increases the accuracy of the ledge detection. 95 | 96 | #The height of the highest ledge that can be grabbed (RELATIVE to player center) 97 | MaxGrabHeight = 1.3 98 | 99 | #Lowest height (relative) 100 | MinGrabHeight = 0.5 101 | #IMPORTANT: min grab height MUST NOT be lower than widest point of player collision mesh! 102 | #So if widest point of C. mesh is .25 units above player center, MinGrabHeight must be at least as high 103 | 104 | #Distance from player center to ledge while hanging 105 | HangDistance = 0.7 #recommended to be very slightly greater than actual distance from center to 106 | #front of hitbox, due to bullet physics margin(or whatever the issue is) 107 | 108 | 109 | HangHeight = 1.3 #How far the ledge should be above the player's center while hanging 110 | SteepestGrabAngle = math.pi/6 #Steepest angle of ledge (relative to player Z-axis) that can be grabbed 111 | 112 | MinGrabSpeed = 4.0 113 | HorizontalGrabSpeed = 6.0 114 | 115 | GrabTolerance = .5 116 | 117 | ClimbLength = .5 118 | ClimbTolerance = .5 119 | 120 | 121 | #Animation settings 122 | 123 | Armature = 'PlayerArmature' #The armature to animate 124 | 125 | ######################### 126 | WalkAnim = Animation() # #Don't change this 127 | ######################### 128 | WalkAnim.name = 'Walk' #Name of the walk action to play 129 | WalkAnim.playType = 'loop' #Play type: 'loop' 'flip' or 'play' 130 | WalkAnim.playSpeed = 2 #Play speed factor (1 = normal speed) 131 | WalkAnim.endFrame = 110 #Last frame of the action 132 | WalkAnim.blendin = 10 #Amount of frames to blend in 133 | 134 | ######################## 135 | RunAnim = Animation() # 136 | ######################## 137 | RunAnim.name = 'Run' 138 | RunAnim.playType = 'loop' 139 | RunAnim.playSpeed = 2 140 | RunAnim.endFrame = 110 141 | RunAnim.blendin = 10 142 | 143 | ######################## 144 | FallAnim = Animation() # 145 | ######################## 146 | FallAnim.name = 'Fall' 147 | FallAnim.playType = 'flip' 148 | FallAnim.endFrame = 48 149 | FallAnim.blendin = 20 150 | 151 | ######################## 152 | IdleAnim = Animation() # 153 | ######################## 154 | IdleAnim.name = 'Stand' 155 | IdleAnim.playType = 'play' 156 | IdleAnim.endFrame = 10 157 | IdleAnim.blendin = 10 158 | 159 | ######################## 160 | HangAnim = Animation() # 161 | ######################## 162 | HangAnim.name = 'Hang' 163 | HangAnim.playType = 'play' 164 | HangAnim.endFrame = 10 165 | HangAnim.blendin = 10 166 | 167 | ######################## 168 | ClimbAnim = Animation()# 169 | ######################## 170 | ClimbAnim.name = 'Climb' 171 | ClimbAnim.endFrame = 56 172 | 173 | ######################## 174 | DeathAnim = Animation()# 175 | ######################## 176 | DeathAnim.name = 'Death' 177 | DeathAnim.endFrame = 100 -------------------------------------------------------------------------------- /red_maze_blue_maze/Controller.py: -------------------------------------------------------------------------------- 1 | import Input 2 | import bge 3 | 4 | def main(): 5 | 6 | logic = bge.logic 7 | cont = logic.getCurrentController() 8 | own = cont.owner 9 | if own['Control Scheme'] == 1: 10 | Input.controlScheme = 'ArrowKeys/WASD' 11 | if own['Control Scheme'] == 2: 12 | Input.controlScheme = 'WASD/Mouse' 13 | if own['Control Scheme'] == 3 and Input.controlScheme != 'Gamepad': 14 | status = Input.compatibleGamepadFound() 15 | if status == 'FOUND_AND_COMPATIBLE': 16 | Input.controlScheme = 'Gamepad' 17 | elif status == 'NOT_FOUND': 18 | print('Could not find gamepad. Reverting to previous control scheme') 19 | elif status == 'INCOMPATIBLE': 20 | print('Gamepad is incompatible. Reverting to previous control scheme') 21 | -------------------------------------------------------------------------------- /red_maze_blue_maze/Gamepad_Input.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import Config as C 3 | from mathutils import Vector 4 | 5 | os = sys.platform[0:3] 6 | 7 | gamepad = bge.logic.joysticks[0] 8 | 9 | joystickThreshold = C.joystickTreshold 10 | 11 | triggerThreshold = C. 12 | 13 | def LSinput(): 14 | if os == 'win' or os == 'dar' or os == 'lin': 15 | if abs(gamepad.axisValues[1]) > joystickThreshold: 16 | axisY = gamepad.axisValues[1] 17 | else: 18 | axisY = 0 19 | if abs(gamepad.axisValues[0]) > joystickThreshold: 20 | axisX = gamepad.axisValues[0] 21 | else: 22 | axisX = 0 23 | 24 | return Vector(axisX, axisY) 25 | 26 | 27 | def RSinput(): 28 | if os == 'win' or os =='lin': 29 | if abs(gamepad.axisValues[3]) > joystickThreshold: 30 | axisY = gamepad.axisValues[3] 31 | else: 32 | axisY = 0 33 | if abs(gamepad.axisValues[4]) > joystickThreshold: 34 | axisX = gamepad.axisValues[4] 35 | else: 36 | axisX = 0 37 | 38 | elif os == 'dar': 39 | if abs(gamepad.axisValues[3]) > joystickThreshold: 40 | axisY = gamepad.axisValues[3] 41 | else: 42 | axisY = 0 43 | if abs(gamepad.axisValues[2]) > joystickThreshold: 44 | axisX = gamepad.axisValues[2] 45 | else: 46 | axisX = 0 47 | 48 | return Vector(axisX, axisY) 49 | 50 | def buttonA(): 51 | if os == 'win': 52 | if 0 in gamepad.activeButtons: 53 | return True 54 | else: 55 | return False 56 | elif os == 'dar': 57 | if 11 in gamepad.activeButtons: 58 | return True 59 | else: 60 | return False 61 | 62 | def buttonB(): 63 | if os == 'win': 64 | if 1 in gamepad.activeButtons: 65 | return True 66 | else: 67 | return False 68 | elif os == 'dar': 69 | if 12 in gamepad.activeButtons: 70 | return True 71 | else: 72 | return False 73 | 74 | def buttonX(): 75 | if os == 'win' or os == 'lin': 76 | if 2 in gamepad.activeButtons: 77 | return True 78 | else: 79 | return False 80 | elif os == 'dar': 81 | if 13 in gamepad.activeButtons: 82 | return True 83 | else: 84 | return False 85 | 86 | def buttonY(): 87 | if os == 'win': 88 | if 3 in gamepad.activeButtons: 89 | return True 90 | else: 91 | return False 92 | elif os == 'dar': 93 | if 14 in gamepad.activeButtons: 94 | return True 95 | else: 96 | return False -------------------------------------------------------------------------------- /red_maze_blue_maze/GravPlanet.py: -------------------------------------------------------------------------------- 1 | from bge import logic 2 | 3 | def main(): 4 | own = logic.getCurrentController().owner 5 | scene = logic.getCurrentScene() 6 | player = scene.objects[own['PlayerName']] 7 | 8 | if 'InRange' not in own: 9 | own['InRange'] = False 10 | 11 | differenceVec = player.worldPosition - own.worldPosition 12 | dist = differenceVec.length 13 | 14 | if dist <= own['PullDist']: 15 | player.alignAxisToVect(differenceVec) 16 | own['InRange'] = True 17 | elif dist >= own['ReleaseDist'] and own['InRange'] == True: 18 | ref = scene.objects[own['ReferenceObj']] 19 | player.alignAxisToVect(ref.worldOrientation[2]) 20 | own['InRange'] = False -------------------------------------------------------------------------------- /red_maze_blue_maze/Gravswitch.py: -------------------------------------------------------------------------------- 1 | from bge import logic 2 | 3 | def main(): 4 | own = logic.getCurrentController().owner 5 | scene = logic.getCurrentScene() 6 | player = scene.objects[own['PlayerName']] 7 | player.alignAxisToVect(own.worldOrientation.col[2]) -------------------------------------------------------------------------------- /red_maze_blue_maze/Input.py: -------------------------------------------------------------------------------- 1 | #This module gathers all input for the player & camera controls 2 | 3 | import bge 4 | import math 5 | import mathutils 6 | import Config 7 | import sys 8 | logic = bge.logic 9 | events = bge.events 10 | render = bge.render 11 | Vector = mathutils.Vector 12 | 13 | controlScheme = 'WASD/Mouse' 14 | 15 | #This function takes two lists, 'a' and 'b', and returns true only if 16 | #each element of 'a' is equal to any element WITHIN the corresponding element of 'b' 17 | #So each element of 'a' can be compared with multiple values 18 | #'a' will generally be an ordinary list, and 'b' will be a list containing lists and/or singular elements 19 | #I use this function to speed up the process of checking what direction is being input 20 | def isEqual(a,b): 21 | equal = True 22 | for i in range(len(a)): 23 | equal2 = False 24 | try: 25 | for x in b[i]: 26 | if x == a[i]: 27 | equal2 = True 28 | except: 29 | if b[i] == a[i]: 30 | equal2 = True 31 | if equal2 == False: 32 | equal = False 33 | return equal 34 | 35 | #If a key is currently pressed, its corresponding element in the logic.keyboard.events 36 | #will be either 1 or 2. Otherwise, it'll be either 0 or 3. The keymask values (look way down in WASD/AKeys input) 37 | #are taken straight from logic.keyboard.events, so they hold the same values. 38 | #I hold both possibilities for either situation in a list, for convenience 39 | p = [1,2] #possible values if the key is pressed 40 | d = [0,3] #not pressed 41 | 42 | def moveInput(): 43 | key = logic.keyboard.events 44 | mouse = logic.mouse.events 45 | global controlScheme 46 | 47 | if controlScheme == 'ArrowKeys/WASD': 48 | inputVec = arrowKeysInput() 49 | if not (isEqual([key[events.ZKEY]],[p]) or isEqual([key[events.LEFTSHIFTKEY]],[p])): 50 | inputVec *= Config.WalkSpeed/Config.MaxSpeed 51 | 52 | elif controlScheme == 'WASD/Mouse': 53 | inputVec = WASDInput() 54 | if not isEqual([mouse[events.RIGHTMOUSE]],[p]): 55 | inputVec *= Config.WalkSpeed/Config.MaxSpeed 56 | 57 | elif controlScheme == 'Gamepad': 58 | try: 59 | return LSinput() 60 | except IndexError: 61 | raise Exception("Index Error raised while gathering gamepad input.\nReverting to WASD/Mouse control scheme") 62 | controlScheme = 'WASD/Mouse' 63 | else: 64 | raise Exception(controlScheme + " is not a valid control scheme") 65 | 66 | return inputVec 67 | 68 | 69 | def jumpInput(): 70 | 71 | key = logic.keyboard.events 72 | mouse = logic.mouse.events 73 | 74 | if controlScheme == 'ArrowKeys/WASD' and (key[events.XKEY] == 1 or key[events.SPACEKEY] == 1): 75 | jump = True 76 | elif controlScheme == 'WASD/Mouse' and mouse[events.LEFTMOUSE] == 1: 77 | jump = True 78 | elif controlScheme == 'Gamepad' and A_BUTTON == 1: 79 | jump = True 80 | else: 81 | jump = False 82 | 83 | return jump 84 | 85 | def jumpIsHeldDown(): 86 | 87 | key = logic.keyboard.events 88 | mouse = logic.mouse.events 89 | 90 | if controlScheme == 'ArrowKeys/WASD' and (isEqual([key[events.XKEY]],[p]) or isEqual([key[events.SPACEKEY]],[p])): 91 | jump = True 92 | elif controlScheme == 'WASD/Mouse' and isEqual([mouse[events.LEFTMOUSE]], [p]): 93 | jump = True 94 | elif controlScheme == 'Gamepad' and isEqual([A_BUTTON], [p]): 95 | jump = True 96 | else: 97 | jump = False 98 | 99 | return jump 100 | 101 | def dropInput(): 102 | 103 | key = logic.keyboard.events 104 | mouse = logic.mouse.events 105 | 106 | if controlScheme == 'ArrowKeys/WASD' and (key[events.ZKEY] == 1 or key[events.LEFTSHIFTKEY] == 1): 107 | drop = True 108 | elif controlScheme == 'WASD/Mouse' and mouse[events.RIGHTMOUSE] == 1: 109 | drop = True 110 | elif controlScheme == 'Gamepad' and B_BUTTON == 1: 111 | drop = True 112 | else: 113 | drop = False 114 | 115 | return drop 116 | 117 | auto = True 118 | 119 | def camInput(): 120 | global controlScheme 121 | key = logic.keyboard.events 122 | mouse = logic.mouse.events 123 | global auto 124 | 125 | if controlScheme == 'ArrowKeys/WASD': 126 | if key[events.LEFTCTRLKEY] == 1: 127 | auto = True 128 | inputVec = WASDInput() 129 | if inputVec.length > 0: 130 | auto = False 131 | 132 | elif controlScheme == 'WASD/Mouse': 133 | inputVec = mouseInput() 134 | if mouse[events.MIDDLEMOUSE] == 1: 135 | auto = not auto 136 | elif controlScheme == 'Gamepad': 137 | try: 138 | inputVec = RSinput() 139 | if Y_BUTTON == 1: 140 | auto = not auto 141 | except IndexError: 142 | raise Exception("Index Error raised while gathering gamepad input.\nPlease use WASD/Mouse control scheme instead.") 143 | controlScheme = 'WASD/Mouse' 144 | else: 145 | raise Exception(controlScheme + " is not a valid control scheme") 146 | 147 | return [-inputVec, auto] 148 | 149 | 150 | 151 | def WASDInput(): 152 | 153 | keymask =[0,0,0,0] 154 | 155 | key = logic.keyboard.events 156 | keymask[0] = key[events.AKEY] 157 | keymask[1] = key[events.WKEY] 158 | keymask[2] = key[events.DKEY] 159 | keymask[3] = key[events.SKEY] 160 | 161 | p = [1,2] 162 | d = [0,3] 163 | 164 | inputVec = mathutils.Vector([0,0]) 165 | 166 | #Primary directions 167 | if isEqual(keymask, [p,d,d,d]): 168 | inputVec = mathutils.Vector([-1,0]) 169 | if isEqual(keymask, [d,p,d,d]): 170 | inputVec = mathutils.Vector([0,1]) 171 | if isEqual(keymask, [d,d,p,d]): 172 | inputVec = mathutils.Vector([1,0]) 173 | if isEqual(keymask, [d,d,d,p]): 174 | inputVec = mathutils.Vector([0,-1]) 175 | 176 | #Diagonal directions 177 | SR2 = 0.5 * math.sqrt(2.0) 178 | 179 | if isEqual(keymask, [p,p,d,d]): 180 | inputVec = mathutils.Vector([-SR2,SR2]) 181 | if isEqual(keymask, [d,p,p,d]): 182 | inputVec = mathutils.Vector([SR2,SR2]) 183 | if isEqual(keymask, [d,d,p,p]): 184 | inputVec = mathutils.Vector([SR2,-SR2]) 185 | if isEqual(keymask, [p,d,d,p]): 186 | inputVec = mathutils.Vector([-SR2,-SR2]) 187 | 188 | return inputVec 189 | 190 | 191 | oldMouseVec = None 192 | mouseSensitivity = Config.mouseSensitivity 193 | mouseSmooth = Config.mouseSmooth 194 | 195 | def mouseInput(): 196 | 197 | cont = logic.getCurrentController() 198 | owner = cont.owner 199 | 200 | centerX = (render.getWindowWidth()//2)/render.getWindowWidth() 201 | centerY = (render.getWindowHeight()//2)/render.getWindowHeight() 202 | 203 | global oldMouseVec 204 | if oldMouseVec == None: 205 | oldMouseVec = mathutils.Vector([0.0,0.0]) 206 | logic.mouse.position = (centerX,centerY) 207 | return mathutils.Vector([0,0]) 208 | 209 | x = logic.mouse.position[0] - centerX 210 | if abs(x) < abs(2/render.getWindowWidth()): x = 0 211 | y = centerY - logic.mouse.position[1] 212 | if abs(y) < abs(2/render.getWindowWidth()): y = 0 213 | newMouseVec = mathutils.Vector([x, y]) 214 | 215 | global mouseSensitivity 216 | 217 | newMouseVec *= mouseSensitivity 218 | 219 | # Smooth movement 220 | global mouseSmooth 221 | 222 | oldMouseVec = oldMouseVec*mouseSmooth + newMouseVec*(1.0-mouseSmooth) 223 | newMouseVec = oldMouseVec 224 | 225 | # Center mouse in game window 226 | logic.mouse.position = (centerX,centerY) 227 | 228 | return mathutils.Vector(newMouseVec) 229 | 230 | def arrowKeysInput(): 231 | keymask =[0,0,0,0] 232 | 233 | key = logic.keyboard.events 234 | keymask[0] = key[events.LEFTARROWKEY] 235 | keymask[1] = key[events.UPARROWKEY] 236 | keymask[2] = key[events.RIGHTARROWKEY] 237 | keymask[3] = key[events.DOWNARROWKEY] 238 | 239 | inputVec = mathutils.Vector([0,0]) 240 | 241 | #Primary directions 242 | if isEqual(keymask, [p,d,d,d]): #If left is pressed, and no others are... 243 | inputVec = mathutils.Vector([-1,0]) 244 | if isEqual(keymask, [d,p,d,d]): #Up 245 | inputVec = mathutils.Vector([0,1]) 246 | if isEqual(keymask, [d,d,p,d]): #Right 247 | inputVec = mathutils.Vector([1,0]) 248 | if isEqual(keymask, [d,d,d,p]): #Down 249 | inputVec = mathutils.Vector([0,-1]) 250 | 251 | #Diagonal directions 252 | SR2 = 0.5 * math.sqrt(2.0) 253 | 254 | if isEqual(keymask, [p,p,d,d]): #Up-Left 255 | inputVec = mathutils.Vector([-SR2,SR2]) 256 | if isEqual(keymask, [d,p,p,d]): #etc 257 | inputVec = mathutils.Vector([SR2,SR2]) 258 | if isEqual(keymask, [d,d,p,p]): 259 | inputVec = mathutils.Vector([SR2,-SR2]) 260 | if isEqual(keymask, [p,d,d,p]): 261 | inputVec = mathutils.Vector([-SR2,-SR2]) 262 | 263 | return inputVec 264 | 265 | os = sys.platform[0:3] 266 | gamepad = bge.logic.joysticks[0] 267 | joystickThreshold = Config.joystickThreshold 268 | 269 | def LSinput(): 270 | if os == 'win' or os == 'dar' or os == 'lin': 271 | if abs(gamepad.axisValues[1]) > joystickThreshold: 272 | axisY = gamepad.axisValues[1] 273 | else: 274 | axisY = 0 275 | if abs(gamepad.axisValues[0]) > joystickThreshold: 276 | axisX = gamepad.axisValues[0] 277 | else: 278 | axisX = 0 279 | 280 | return Vector([axisX, -axisY]) 281 | 282 | 283 | def RSinput(): 284 | if os == 'win' or os =='lin': 285 | if abs(gamepad.axisValues[3]) > joystickThreshold: 286 | axisY = gamepad.axisValues[3] 287 | else: 288 | axisY = 0 289 | if abs(gamepad.axisValues[4]) > joystickThreshold: 290 | axisX = gamepad.axisValues[4] 291 | else: 292 | axisX = 0 293 | 294 | elif os == 'dar': 295 | if abs(gamepad.axisValues[3]) > joystickThreshold: 296 | axisY = gamepad.axisValues[3] 297 | else: 298 | axisY = 0 299 | if abs(gamepad.axisValues[2]) > joystickThreshold: 300 | axisX = gamepad.axisValues[2] 301 | else: 302 | axisX = 0 303 | 304 | return Vector([axisX, -axisY]) 305 | 306 | D_PAD_UP = 0 307 | D_PAD_DOWN = 0 308 | D_PAD_RIGHT = 0 309 | D_PAD_LEFT = 0 310 | L_TRIGGER = 0 311 | R_TRIGGER = 0 312 | A_BUTTON = 0 313 | B_BUTTON = 0 314 | X_BUTTON = 0 315 | Y_BUTTON = 0 316 | L_BUMPER = 0 317 | R_BUMPER = 0 318 | BACK_BUTTON = 0 319 | START_BUTTON = 0 320 | L_JS_BUTTON = 0 321 | R_JS_BUTTON = 0 322 | 323 | triggerThreshold = 0.25 324 | 325 | def pollButtons(): 326 | global D_PAD_UP 327 | global D_PAD_DOWN 328 | global D_PAD_RIGHT 329 | global D_PAD_LEFT 330 | global L_TRIGGER 331 | global R_TRIGGER 332 | global A_BUTTON 333 | global B_BUTTON 334 | global X_BUTTON 335 | global Y_BUTTON 336 | global L_BUMPER 337 | global R_BUMPER 338 | global BACK_BUTTON 339 | global START_BUTTON 340 | global L_JS_BUTTON 341 | global R_JS_BUTTON 342 | 343 | #If running on Windows: 344 | if os == 'win': 345 | #Triggers: 346 | if gamepad.axisValues[2] > triggerThreshold: 347 | L_TRIGGER = nextButtonVal(True, L_TRIGGER) 348 | else: 349 | L_TRIGGER = nextButtonVal(False,L_TRIGGER) 350 | if gamepad.axisValues[2] < -triggerThreshold: 351 | R_TRIGGER = nextButtonVal(True, R_TRIGGER) 352 | else: 353 | R_TRIGGER = nextButtonVal(False, R_TRIGGER) 354 | #D-Pad 355 | if gamepad.hatValues[0] == 1: 356 | D_PAD_UP = nextButtonVal(True, D_PAD_UP) 357 | else: 358 | D_PAD_UP = nextButtonVal(False, D_PAD_UP) 359 | if gamepad.hatValues[0] == 2: 360 | D_PAD_RIGHT = nextButtonVal(True,D_PAD_RIGHT) 361 | else: 362 | D_PAD_RIGHT = nextButtonVal(False, D_PAD_RIGHT) 363 | if gamepad.hatValues[0] == 8: 364 | D_PAD_LEFT = nextButtonVal(True,D_PAD_LEFT) 365 | else: 366 | D_PAD_LEFT = nextButtonVal(False, D_PAD_LEFT) 367 | if gamepad.hatValues[0] == 4: 368 | D_PAD_DOWN = nextButtonVal(True,D_PAD_DOWN) 369 | else: 370 | D_PAD_DOWN = nextButtonVal(False, D_PAD_DOWN) 371 | #Buttons 372 | if 0 in gamepad.activeButtons: 373 | A_BUTTON = nextButtonVal(True,A_BUTTON) 374 | else: 375 | A_BUTTON = nextButtonVal(False, A_BUTTON) 376 | if 1 in gamepad.activeButtons: 377 | B_BUTTON = nextButtonVal(True, B_BUTTON) 378 | else: 379 | B_BUTTON = nextButtonVal(False, B_BUTTON) 380 | if 2 in gamepad.activeButtons: 381 | X_BUTTON = nextButtonVal(True, X_BUTTON) 382 | else: 383 | X_BUTTON = nextButtonVal(False, X_BUTTON) 384 | if 3 in gamepad.activeButtons: 385 | Y_BUTTON = nextButtonVal(True, Y_BUTTON) 386 | else: 387 | Y_BUTTON = nextButtonVal(False, Y_BUTTON) 388 | if 4 in gamepad.activeButtons: 389 | L_BUMPER = nextButtonVal(True, L_BUMPER) 390 | else: 391 | L_BUMPER = nextButtonVal(False, L_BUMPER) 392 | if 5 in gamepad.activeButtons: 393 | R_BUMPER = nextButtonVal(True, R_BUMPER) 394 | else: 395 | R_BUMPER = nextButtonVal(False, R_BUMPER) 396 | if 6 in gamepad.activeButtons: 397 | BACK_BUTTON = nextButtonVal(True, BACK_BUTTON) 398 | else: 399 | BACK_BUTTON = nextButtonVal(False, BACK_BUTTON) 400 | if 7 in gamepad.activeButtons: 401 | START_BUTTON = nextButtonVal(True, START_BUTTON) 402 | else: 403 | START_BUTTON = nextButtonVal(False, START_BUTTON) 404 | if 8 in gamepad.activeButtons: 405 | L_JS_BUTTON = nextButtonVal(True, L_JS_BUTTON) 406 | else: 407 | L_JS_BUTTON = nextButtonVal(False,L_JS_BUTTON) 408 | if 9 in gamepad.activeButtons: 409 | L_JS_BUTTON = nextButtonVal(True, L_JS_BUTTON) 410 | else: 411 | R_JS_BUTTON = nextButtonVal(False,R_JS_BUTTON) 412 | #If running on MAC 413 | elif os == 'dar': 414 | #Triggers 415 | if gamepad.axisValues[4] > triggerThreshold: 416 | L_TRIGGER = nextButtonVal(True, L_TRIGGER) 417 | else: 418 | L_TRIGGER = nextButtonVal(False,L_TRIGGER) 419 | if gamepad.axisValues[5] > triggerThreshold: 420 | R_TRIGGER = nextButtonVal(True, R_TRIGGER) 421 | else: 422 | R_TRIGGER = nextButtonVal(False, R_TRIGGER) 423 | #D-Pad 424 | if 0 in gamepad.activeButtons: 425 | D_PAD_UP = nextButtonVal(True, D_PAD_UP) 426 | else: 427 | D_PAD_UP = nextButtonVal(False, D_PAD_UP) 428 | if 1 in gamepad.activeButtons: 429 | D_PAD_DOWN = nextButtonVal(False, D_PAD_DOWN) 430 | else: 431 | D_PAD_DOWN = nextButtonVal(False, D_PAD_DOWN) 432 | if 2 in gamepad.activeButtons: 433 | D_PAD_LEFT = nextButtonVal(True,D_PAD_LEFT) 434 | else: 435 | D_PAD_LEFT = nextButtonVal(False, D_PAD_LEFT) 436 | if 3 in gamepad.activeButtons: 437 | D_PAD_RIGHT = nextButtonVal(True,D_PAD_RIGHT) 438 | else: 439 | D_PAD_RIGHT = nextButtonVal(False, D_PAD_RIGHT) 440 | #Buttons 441 | if 4 in gamepad.activeButtons: 442 | START_BUTTON = nextButtonVal(True, START_BUTTON) 443 | else: 444 | START_BUTTON = nextButtonVal(False, START_BUTTON) 445 | if 5 in gamepad.activeButtons: 446 | BACK_BUTTON = nextButtonVal(True, BACK_BUTTON) 447 | else: 448 | BACK_BUTTON = nextButtonVal(False, BACK_BUTTON) 449 | if 6 in gamepad.activeButtons: 450 | L_JS_BUTTON = nextButtonVal(True, L_JS_BUTTON) 451 | else: 452 | L_JS_BUTTON = nextButtonVal(False,L_JS_BUTTON) 453 | if 7 in gamepad.activeButtons: 454 | R_JS_BUTTON = nextButtonVal(True, R_JS_BUTTON) 455 | else: 456 | R_JS_BUTTON = nextButtonVal(False,R_JS_BUTTON) 457 | if 8 in gamepad.activeButtons: 458 | L_BUMPER = nextButtonVal(True, L_BUMPER) 459 | else: 460 | L_BUMPER = nextButtonVal(False, L_BUMPER) 461 | if 9 in gamepad.activeButtons: 462 | R_BUMPER = nextButtonVal(True, R_BUMPER) 463 | else: 464 | R_BUMPER = nextButtonVal(False, R_BUMPER) 465 | #if 10 in xbox.activeButtons: 466 | #HOME_BUTTON = nextButtonVal(True, HOME_BUTTON) 467 | #else: 468 | #HOME_BUTTON = nextButtonVal(False, HOME_BUTTON) 469 | if 11 in gamepad.activeButtons: 470 | A_BUTTON = nextButtonVal(True,A_BUTTON) 471 | else: 472 | A_BUTTON = nextButtonVal(False, A_BUTTON) 473 | if 12 in gamepad.activeButtons: 474 | B_BUTTON = nextButtonVal(True, B_BUTTON) 475 | else: 476 | B_BUTTON = nextButtonVal(False, B_BUTTON) 477 | if 13 in gamepad.activeButtons: 478 | X_BUTTON = nextButtonVal(True, X_BUTTON) 479 | else: 480 | X_BUTTON = nextButtonVal(False, X_BUTTON) 481 | if 14 in gamepad.activeButtons: 482 | Y_BUTTON = nextButtonVal(True, Y_BUTTON) 483 | else: 484 | Y_BUTTON = nextButtonVal(False, Y_BUTTON) 485 | #If running on Linux 486 | elif os == 'lin': 487 | #Triggers: 488 | if gamepad.axisValues[2] > triggerThreshold: 489 | L_TRIGGER = nextButtonVal(True, L_TRIGGER) 490 | else: 491 | L_TRIGGER = nextButtonVal(False,L_TRIGGER) 492 | if gamepad.axisValues[5] < triggerThreshold: 493 | R_TRIGGER = nextButtonVal(True, R_TRIGGER) 494 | else: 495 | R_TRIGGER = nextButtonVal(False, R_TRIGGER) 496 | #D-Pad 497 | if 11 in gamepad.activeButtons: 498 | D_PAD_LEFT = nextButtonVal(True,D_PAD_LEFT) 499 | else: 500 | D_PAD_LEFT = nextButtonVal(False, D_PAD_LEFT) 501 | if 12 in gamepad.activeButtons: 502 | D_PAD_RIGHT = nextButtonVal(True,D_PAD_RIGHT) 503 | else: 504 | D_PAD_RIGHT = nextButtonVal(False, D_PAD_RIGHT) 505 | if 13 in gamepad.activeButtons: 506 | D_PAD_UP = nextButtonVal(True, D_PAD_UP) 507 | else: 508 | D_PAD_UP = nextButtonVal(False, D_PAD_UP) 509 | if 14 in gamepad.hatValues: 510 | D_PAD_DOWN = nextButtonVal(True,D_PAD_DOWN) 511 | else: 512 | D_PAD_DOWN = nextButtonVal(False, D_PAD_DOWN) 513 | #Buttons 514 | if 0 in gamepad.activeButtons: 515 | A_BUTTON = nextButtonVal(True,A_BUTTON) 516 | else: 517 | A_BUTTON = nextButtonVal(False, A_BUTTON) 518 | if 1 in gamepad.activeButtons: 519 | B_BUTTON = nextButtonVal(True, B_BUTTON) 520 | else: 521 | B_BUTTON = nextButtonVal(False, B_BUTTON) 522 | if 2 in gamepad.activeButtons: 523 | X_BUTTON = nextButtonVal(True, X_BUTTON) 524 | else: 525 | X_BUTTON = nextButtonVal(False, X_BUTTON) 526 | if 3 in gamepad.activeButtons: 527 | Y_BUTTON = nextButtonVal(True, Y_BUTTON) 528 | else: 529 | Y_BUTTON = nextButtonVal(False, Y_BUTTON) 530 | if 4 in gamepad.activeButtons: 531 | L_BUMPER = nextButtonVal(True, L_BUMPER) 532 | else: 533 | L_BUMPER = nextButtonVal(False, L_BUMPER) 534 | if 5 in gamepad.activeButtons: 535 | R_BUMPER = nextButtonVal(True, R_BUMPER) 536 | else: 537 | R_BUMPER = nextButtonVal(False, R_BUMPER) 538 | if 6 in gamepad.activeButtons: 539 | BACK_BUTTON = nextButtonVal(True, BACK_BUTTON) 540 | else: 541 | BACK_BUTTON = nextButtonVal(False, BACK_BUTTON) 542 | if 7 in gamepad.activeButtons: 543 | START_BUTTON = nextButtonVal(True, START_BUTTON) 544 | else: 545 | START_BUTTON = nextButtonVal(False, START_BUTTON) 546 | #if 8 in xbox.activeButtons: 547 | # HOME_BUTTON = nextButtonVal(True, HOME_BUTTON) 548 | #else: 549 | # HOME_BUTTON = nextButtonVal(False, HOME_BUTTON) 550 | if 9 in gamepad.activeButtons: 551 | L_JS_BUTTON = nextButtonVal(True, L_JS_BUTTON) 552 | else: 553 | L_JS_BUTTON = nextButtonVal(False,L_JS_BUTTON) 554 | if 10 in gamepad.activeButtons: 555 | R_JS_BUTTON = nextButtonVal(True, R_JS_BUTTON) 556 | else: 557 | R_JS_BUTTON = nextButtonVal(False,R_JS_BUTTON) 558 | 559 | def nextButtonVal(current, last): 560 | if current == True: 561 | if last == 1 or last == 2: 562 | return 2 563 | else: 564 | return 1 565 | else: 566 | if last == 0 or last == 3: 567 | return 0 568 | else: 569 | return 3 570 | 571 | def compatibleGamepadFound(): 572 | if gamepad == None: 573 | return 'NOT_FOUND' 574 | try: 575 | pollButtons() 576 | LSinput() 577 | RSinput() 578 | except IndexError: 579 | return 'INCOMPATIBLE' 580 | return 'FOUND_AND_COMPATIBLE' 581 | 582 | -------------------------------------------------------------------------------- /red_maze_blue_maze/Instructions.txt: -------------------------------------------------------------------------------- 1 | You can easily apply this template to an existing .blend file, if you wish. Just remember to place all the Python modules in the same file directory(folder, generally) as the .blend. Or, you could append/link your models into the template and go from there. In any case, I'll describe the process of applying the template to your character. 2 | 3 | The player's collision is handled by a diamond-shaped collision mesh that hovers above the ground (for wall collisions), and a raycast in the script that detects the floor and pins the player to it. The collision mesh actually has no friction; sliding along the floor is not a concern because 4 | ground collision is handled by a raycast. Another benefit of the raycast method is that it lets the player easily walk over small steps. 5 | 6 | So, to make a collision mesh for your character, create a diamond shaped like the one in the template. The pointy ends are critical; flat ends will catch on ledges. Make the top vertex of the diamond as high as the top of the character's head, and the lower vertex of the diamond about as low as the highest step you want the player to be able to walk over (this will be important later). Even if you don't want him to walk over any steps, the lower vertex should be a bit higher than ground level. You could also just copy the collision mesh from my template, and scale and position it to suit your character model. If you do that, you'll be good to go as far as logic bricks and physics settings, but you'll still need to set up some Config settings- see Config Settings below. 7 | 8 | Make sure the collision mesh is aligned with the character, and parent the character's armature to it. Then make sure the character's visual mesh is parented to his armature. Make sure the visual mesh has it's physics mode set to "No Collision". Set the collision mesh's physics mode to "Dynamic", and enable the "actor", "invisible", and "no sleeping" physics options. 9 | 10 | It's also a good idea to vertex parent the skybox to the player's collision mesh, like I did in the template (althoug the skybox is hidden, press alt+H to see it in the editor). 11 | 12 | Now, you need to add some logic bricks to the collision mesh, which handles all the logic for the character. Just copy the logic brick setup exactly from the collision mesh in the template. Than make sure the collision mesh has a game property called "Player". 13 | 14 | 15 | Config Settings: 16 | 17 | Now you the collision mesh set up properly, but you need to set some config settings. Open the file Config.py provided in the zip folder. Right at the top, you'll see the 'Player Settings' subheading- look there. The first setting you need to change is "Cam". Set it to the name of the camera object in your scene (if you have no camera, make one now. it's initial position and orientation are unimportant). Of course, if you're using my template as a base to append/link your own content into, you should be able to leave this setting as is. Right below that, you'll see the "Height" setting. This setting is important to get right. It dictates how far above ground level the center (and by center I mean object origin) of the collision mesh object should be placed (in Blender units_. If this isn't set right, the character will hover above the ground or sink into it. You can make a guess for now, and fine-tune it later once you test it. 18 | 19 | The next setting you need to set is 'SpawnObjectName'. This dicatates the name of the object at which the player will spawn at at the start of the game (should probably be an Empty. If you don't have one, add one now). Once again, if you're just adding content into the tamplate .blend file, you might not have to change this, as there is already a spawn empty in the template. 20 | 21 | Then, look under the 'Stair and step settings' sunheading. Find the "Step" setting. Enter the height (in Blender units) of the lowest step you want your character to be able to walk down without detaching from the ground. It's important that the lowest point of the collision mesh be about this high above ground level, otherwise your character may stuck trying to walk up steps. 22 | 23 | Next, go and set the 'Ledge Hang Settings' to the best of your knowledge. Remember, you can guess now, and fine-tune later. It's important to note that max and min grab height are set RELATIVE to player (collision mesh) center. So max grab height is actually the maximum distance above the player center a ledge can be to be grabbed. The most important setting to get right is "HangDistance". It should be close to, but very slightly greater than actual distance from center to the very furthest forward part of player collision mesh. Also set "HangHeight", which is again defined relative to player center (the player's hands should be about this high during his armature hanging action). 24 | 25 | The last thing you need to do is make sure all the 'Animation Settings' are set properly. First, make sure the "Armature" setting is set to the name of the character's armature. Then, there are several animations you need to configure (Walk, Run, Fall, Idle, Hang, Climb, Death). Make sure the name propertyof each of these animations is set to the name of the corresponding action for your armature. If you don't have, for instance, a fall animation, you could use your idle animation as placeholder, or use your walk animation as placeholder for the run anim. 26 | 27 | The climb armature action needs to be animated in a very specific way to look right with this template. It should start with the player in his regular hanging position, with his hands level to the ledge, and then climbing up and over the ledge, with the ENTIRE ARMATURE moving upwards. He should move upwards by an amount equal to the sum of the "Height" and the "HangHeight" settings. He should move forwards by an amount equal to the sum of the "HangDistance" and "ClimbLength" settings. The action should end with the player standing in his idle pose, but offset by the previously mentioned amounts. 28 | 29 | One last thing to mention about ledge hand settings and the climb anim is that the amount of time to complete the ledge climbing sequence will always be equal to the number of frames specified by "ClimbAnim.endFrame". -------------------------------------------------------------------------------- /red_maze_blue_maze/Notes.txt: -------------------------------------------------------------------------------- 1 | ##### Changing default control scheme, making an in-game controls menu 2 | 3 | The Input.py module is programmed to, by default, get it's input from the mouse and WASD keys. So I set up a controller object 4 | (It's shaped like a giant video game controller in the editor, you can't miss it (although it's invisible in-game)). Anyway, 5 | this controller object uses a script (Controller.py) to access the Input.py module and change it's control input settings 6 | (there's a variable in the Input.py module that determines what control scheme is used) according to the controller object's 7 | integer game property called "Control Scheme". Every frame, the controller object runs the script to change the control setting 8 | according to this property. A property value of 1 indicates the "ArrowKeys/WASD" control scheme, a value of 2 indicates the 9 | "WASD/Mouse control scheme", and a value of 3 indicates the "Gamepad" control scheme (probably only works for Xbox 360 10 | controllers). So, to change the default control scheme, just change the value of this property in the logic panel. 11 | 12 | You might wonder why I set up the controls cheme to be controlled by a game property rather than the Config file (like every 13 | other setting in the template). The reason is that this method allows people with no Python coding knowledge to set up their 14 | own method for changing the control scheme in-game using only logic bricks (Just use the property actuator to update the value 15 | of the "Control Scheme: property). Currently the controller object has a logic bricks set up to use the F1, F2, and F3 keys to 16 | set the property to 1,2, or 3 respectively. You can replace those logic bricks with your own logic setup. This allows you to 17 | create a menu for changing the controls in-game, or use whatever method you want. 18 | 19 | 20 | ##### Creating deadly objects and falling death zones 21 | 22 | To create an object that kills the player, give it a game property called "Death", and he will die upon touching it. 23 | 24 | The setup for a falling death zone is more complex. First I'll explain how to model one, then how to set it up. 25 | 26 | To model a falling death zone, I suggest you make a large box enclosing the whole level, with triangle mesh collision bounds 27 | (because triangle mesh collision bounds are hollow, unlike cube or sphere bounds). You can use more complex shapes than a box, or 28 | even use multiple objects as death zones. 29 | 30 | To make it kill the player, add a property called "Death" to the death zone. Also, you would normally want a falling death zone 31 | to have no collision, however, setting it's physics mode to "No Collision" will make the player ignore it entirely. To get 32 | around this, set it's collision mode to Static and enable the "ghost" option in the physics panel. Then add to it a game property 33 | called "NoStand". Now the player will pass through it instead of landing on it. You can also apply this method to make intangible 34 | deadly objects, like flames, that kill the player but don't collide with him. 35 | 36 | There is a death zone enclosing the whole level in the template file, it is just hidden. Press alt+H to view it in the editor. 37 | 38 | 39 | ##### Creating a checkpoint, changing checkpoint activation distance 40 | 41 | Simply create the checkpoint object, and give it a game property called "Checkpoint" 42 | Once the player gets within 3 units of the checkpoint, it will become activated and he will respawn at it upon death. 43 | (you can adjust the activation distance by changing the distance fields on the Near sensor (named "Checkpoint") of the 44 | player's collision mesh. You could replace the near sensor with a sensor of a different type, but it must also be 45 | named "Checkpoint") 46 | 47 | The checkpoint object should be set to Static physics mode(if you set it to No Collision mode, the player will never detect it). 48 | However, if you want the player to be able to pass through it, you can enable the "ghost" option in the physics panel (make sure 49 | the "actor" option just above it is also enabled), then give it a game property called "NoStand". 50 | 51 | Note that when a player respawns at a checkpoint, he will respawn in the exact location of it's origin. So make sure to place its 52 | origin far enough above the ground so that the player won't clip into the ground. Also, if you didn't make the checkpoint a "ghost", 53 | then the origin should be placed far enough away from the checkpoint so that the player won't clip into the checkpoint itself. 54 | 55 | 56 | ##### Resetting the scene upon respawning 57 | 58 | Note that there is an option in the Config.py file called "ResetSceneUponDeath". By default it is set to False, so when the player 59 | dies and respawns the scene will continue as usual. If you would like the scene to reset whenever the player respawns, set it to 60 | True. 61 | 62 | 63 | ##### Creating Gravity Switches 64 | 65 | Each gravswitch in my demo level consists of two parts: a visible mesh object, and an empty object that handles all the 66 | logic (so if you move one part, you need to move the other). This is so you can rotate the visual mesh and empty object 67 | independently (this is important because the empty's rotation determines the resultant direction of gravity). 68 | If you create a game based off of my template, here's how to add gravity switches to it. 69 | 70 | You could append one of my gravswitch empties into your game and duplicate it, or you could set up your own from scratch. 71 | To set up your own gravity switch from scratch, create an empty object. In the logic panel add a near sensor that detects 72 | when the player gets close to the switch (you could use any sensor, if you want), and hook it up to a Python controller that 73 | executes "Gravswitch.main"(the python controller must be set to module mode). Finally, add a String property called 74 | "PlayerName", and in the string field, type in the name of the player's collision mesh. The gravswitch script will read this 75 | property to know which object to mess with. 76 | 77 | Now, rotate the gravswitch so that it's local negative Z axis points in the desired direction of gravity. Now, whenever 78 | the player gets close to the gravswitch, his direction of gravity will change. 79 | 80 | 81 | ##### Creating a Planetoid 82 | 83 | A planetoid consists of only one object: the planetoid itself. When the player gets within a specified distance of the planetoid, 84 | he will be sucked into its gravity. He can escape by getting a specified distance away from the planet (i.e. by jumping on a 85 | spring). Here's how you set one up. 86 | 87 | First, make the planetoid mesh object. Then, in the logic panel, add an always sensor, and enable true level triggering 88 | for it. Link it to a Python controller that executes "GravPlent.main" (python controller must be set to module mode). 89 | Add a String property called "PlayerName", and in the string field, type in the name of the player's collision mesh. 90 | Add a Float property called "PullDist", and give it the value of the distance from the planet at which you want the player 91 | to be captured by it's gravitational field. 92 | Add a Float property called "ReleaseDist", and give it the value of the distance from the planet at which you want the 93 | player to be released from the planet's gravity, when he's already in its gravitational field. 94 | 95 | Finally, add a String property called "ReferenceObj". Assign to it the name of the object you want to use as the reference object. 96 | The orientation of the reference object determines the direction gravity will be pointing when the player is released from the 97 | planet's gravitational field, similar to how a gravswitch works. 98 | 99 | 100 | ##### Stair behavior 101 | 102 | The StepSmoothing variable not only smooths the movement on steps, but it also happens to 103 | determine how steep your staircases can be without running the risk of the player detaching from 104 | them when descending. The ratio between the horizontal/vertical distance of each step should not be less than 105 | the StepSmoothing factor, otherwise you run the risk of the player coming off of the stairs. 106 | 107 | So, if you have a stepSmooth value of .5, then the horizontal distance of each step on a staircase can not be less 108 | than 0.5 times the vertical distance of the step. 109 | 110 | Note that this rule doesn't apply to singular steps, only staircases. And, of course, if a step is taller than the 111 | Step config value, the player will always detach from it when stepping down it. 112 | 113 | 114 | ##### Camera "obstacle avoidance" 115 | 116 | The camera system is capable of a crude form of obstacle avoidance: it just zooms in if something gets between it 117 | and the player. For an object to block the camera in this manner, you must give it a logic property called "CamBlock". 118 | 119 | 120 | ##### Speed modifier surfaces 121 | 122 | To add a speed modifier to a surface, add a Float game property to it's object called 'SpeedModifier', and set it's 123 | value to the desired speed multiplier (0.5 will make the player go half as fast, for instance). This is useful for stairs, 124 | because it would look unnatural for the player to dash up stairs at full speed. I use a value of 0.5 for staircases. 125 | 126 | 127 | ##### Un-grabbable and un-stand-able surfaces 128 | 129 | If you want a certain surface to not be grabbable as a ledge, give it a logic property called "NoGrab". 130 | Likewise, if you want a surface to not be stand-able, give it a property called "NoStand". If the player tries to stand on 131 | an un-stand-able surface, he wil remain in his fall state and just slide down it. 132 | 133 | ##### This template was designed for 60 FPS 134 | 135 | Changing the target framerate won't break any functionality, but it will cause speeds and velocities to differ a bit (which ideally shouldn't happen). Of course, it works just fine with performance-related frame drops. -------------------------------------------------------------------------------- /red_maze_blue_maze/Player.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | ###Initial Stuff### 3 | 4 | import bge 5 | import math 6 | import mathutils 7 | import TMath 8 | import AnimationHelper 9 | import Input 10 | import Config as C 11 | import copy 12 | Vector = mathutils.Vector 13 | logic = bge.logic 14 | events = bge.events 15 | GD = logic.globalDict 16 | 17 | cont = logic.getCurrentController() 18 | own = cont.owner 19 | sce = logic.getCurrentScene() 20 | #Get the camera(specified by the player object's 'cam' property) 21 | cam = sce.objects[C.Cam] 22 | orient = own.worldOrientation 23 | 24 | ##Here, I create various object properties that the script will need later, if they don't already exist 25 | 26 | if not 'State' in own: 27 | own['State'] = 'idle' 28 | state = own['State'] 29 | #own['PlatformVelocity'] stores the velocity of the platform the player is stainding on (from the last frame) 30 | #It's LOCAL to the player's orientation, by the way 31 | if not 'PlatformVelocity' in own: 32 | own['PlatformVelocity'] = [0,0,0] 33 | platformVelocity = own['PlatformVelocity'] 34 | #own['PrevOffset'] is used during ledge grab/hang/climb operations. I'll explain it the relevant sections 35 | if not 'PrevOffset' in own: 36 | own['PrevOffset'] = None 37 | #own['NoMoreGrab'] is used to temporarily toggle on/off ledgeGrab, so you don't re-grab a ledge after falling off. 38 | if not 'NoMoreGrab' in own: 39 | own['NoMoreGrab'] = False 40 | #own['RespawnCount'] counts the frames after you die, and when it reaches a certain value you respawn 41 | if not 'RespawnCount' in own: 42 | own['RespawnCount'] = 0 43 | 44 | ##Spawning at the start of scene 45 | 46 | #own['RespawnObj'] stores the place to respawn at (either the initial spawn point or last touched checkpoint) 47 | #If RespawnObj does not yet exist in own(e.g. this is the first frame this script has been run), spawn player at spawn object. 48 | #If Config.ResetSceneUpon death is enabled, then when player dies, respawnObj position and location are stashed in logic.globalDict. 49 | #Upon restart of scene, position and location are read from globalDict, and 'RespawnObj' is set to None, purely to indicate 50 | #that the player has finished respawning, so he won't respawn repeatedly. 51 | if not 'RespawnObj' in own: 52 | if 'PlayerSpawnPosition' in GD and GD['SpawnPositionScene'] == sce.name: 53 | own.worldPosition = GD['PlayerSpawnPosition'] 54 | own.worldOrientation = GD['PlayerSpawnOrientation'] 55 | own['RespawnObj'] = None 56 | else: 57 | own['RespawnObj'] = sce.objects[C.SpawnObjectName] 58 | own.worldPosition = own['RespawnObj'].worldPosition 59 | own.worldOrientation = own['RespawnObj'].worldOrientation 60 | 61 | maxSpd = C.MaxSpeed 62 | walkSpd = C.WalkSpeed 63 | #get and tweak the turning smoothness factor 64 | smooth = (C.TurnSmoothness * .75) + .125 65 | 66 | 67 | ###Calculating The Forward Direction Vector(relative to the camera)### 68 | 69 | #Get the direction the camera is facing(as an XYZ vector) 70 | forwardAxis = -1 * mathutils.Vector(map(lambda x: x[2], cam.worldOrientation)) 71 | #Get the X, Y, and Z axes of the player object 72 | axisX = mathutils.Vector(orient.col[0]) 73 | axisY = mathutils.Vector(orient.col[1]) 74 | axisZ = mathutils.Vector(orient.col[2]) 75 | #Flatten the camera's direction onto the player object's local XY plane 76 | forwardAxis = TMath.VecToPlane(forwardAxis, axisX, axisY) 77 | 78 | 79 | ###Get Input### 80 | if Input.controlScheme == 'Gamepad': 81 | Input.pollButtons() 82 | 83 | input = Input.moveInput() 84 | 85 | isMoving = (input.length != 0) 86 | inputVec = input 87 | jump = Input.jumpInput() 88 | heldJump = Input.jumpIsHeldDown() 89 | drop = Input.dropInput() 90 | 91 | ###Floor Raycast### ### 92 | 93 | #The starting point of the ray is the player's position 94 | pos = own.worldPosition 95 | 96 | #Get the per-frame vertical offset caused by the player's velocity 97 | #(to add to the length of the ray, so the player doesn't fall into the ground at high fall speeds 98 | velOffset = own.getLinearVelocity(1)[2]/60 99 | 100 | #The ending point is the player's position minus (the player's height times the player's local z-axis(unit length) 101 | #This means the ray will always point downwards along the player's local z-axis 102 | pos2 = [pos[i] - C.Height*axisZ[i] for i in range(3)] 103 | #If the player is on the ground, extend the length of the ray by the player's step size, plus velOffset 104 | if own['State'] == 'walk' or own['State'] == 'run' or own['State'] == 'idle': 105 | pos2 = [pos2[i] - (1.001*C.Step-velOffset)*axisZ[i] for i in range(3)] 106 | #If player is in the air, add all axes of player's linear velocity to ray end position 107 | elif state == 'fall' or state == 'jump': 108 | pos2 = [pos2[i] + own.getLinearVelocity(0)[i]/60 for i in range(3)] 109 | #Do raycast 110 | ray = own.rayCast(pos2, pos, 0, "", 1, 1) 111 | 112 | #Determine whether the detected floor is standable 113 | noStand = False 114 | if ray[0] != None and 'NoStand' in ray[0]: 115 | noStand = True 116 | 117 | ###Jump Detection### 118 | 119 | justJumped = False 120 | #If X has just been pressed and the player is not in the fall or jump state 121 | if jump == True and state != 'fall' and state != 'jump': 122 | #Set justJumped to True 123 | justJumped = True 124 | 125 | ###Ledge Grab Raycasts### 126 | 127 | ###Note: To see test visualization of all the rays used during ledge grab, uncomment lines 167, 238, and 239 128 | #This can help you understand the code better. 129 | 130 | justGrabbed = False #false by default 131 | #Recover the ledgeRay results, wallRay results, and riseVec from the previous frame, if they exist. This lets us keep reusing the same 132 | #hit point data over many frames during ledge grabbing 133 | if 'LedgeRay' in own: ledgeRay = own['LedgeRay'] 134 | if 'WallRay' in own: wallRay = own['WallRay'] 135 | if 'RiseVec' in own: riseVec = own['RiseVec'] 136 | 137 | #While in fall state, do ledge detection 138 | if own['State'] == 'fall' and own['NoMoreGrab'] == False and C.UseLedgeHang == True: 139 | #I'm going to cast a series of downward rays to detect ledges 140 | 141 | #pos1 is the starting position of the first ray in the series 142 | #pos1 = player position offset by max grab height and min ray distance. 143 | pos1 = [pos[i] + C.MaxGrabHeight*axisZ[i] for i in range(3)] 144 | pos1 = [pos1[i] + C.MinRayDistance*axisY[i] for i in range(3)] 145 | 146 | #offsetVec is the vector that is added to the star position of each ray to determine the end position 147 | #It goes straight down along -Z axis, and has length of grab range+velOffset 148 | offsetVec = [(-(C.MaxGrabHeight-C.MinGrabHeight)+velOffset)*axisZ[i] for i in range(3)] 149 | #rayStepVec is the vector by which each successive vector in the series is offset from the last 150 | rayStepVec = [(C.MaxRayDistance-C.MinRayDistance)*axisY[i]/(C.NumRays-1) for i in range(3)] 151 | 152 | #pos2 is the ending position of the first ray in the series 153 | pos2 = [pos1[i] + offsetVec[i] for i in range(3)] 154 | 155 | #Calculate how far each successive ray endpoint must rise above the next, so the line of rays matches the incline 156 | #of the steepest allowed grab angle (this is necessary for complicated reasons) 157 | tan = math.tan(C.SteepestGrabAngle*1.01) 158 | run = (C.MaxRayDistance-C.MinRayDistance)/(C.NumRays-1) 159 | rise = max((tan+.1),.1) * run 160 | 161 | upStepVec = rise * axisZ 162 | #Then add it to the rayStepVec to get rayStepVec 2 163 | rayStepVec2 = [rayStepVec[i]+ upStepVec[i] for i in range(3)] 164 | 165 | #for each ray in the series 166 | for i in range(C.NumRays): 167 | #bge.render.drawLine(pos1, pos2, [0,1,1]) #this is for testing 168 | ledgeRay = own.rayCast(pos2, pos1, 0, "", 1, 1) #cast the ray from the start to the end positions 169 | if ledgeRay[0] != None: #if the ray hit something 170 | normal = ledgeRay[2] 171 | angle = normal.angle(axisZ) #get angle between hit normal and local Z-axis 172 | #if the hit normal is not too steep to grab ledge, and ledge is grabbable... 173 | if angle <= C.SteepestGrabAngle and not 'NoGrab' in ledgeRay[0]: 174 | #Obstruction testing 175 | #Now I need to make sure there isn't a wall between the player and the ledge. To do this, 176 | #I'll shoot a ray that's level with a point just above the hit point, and the ray will 177 | #start above the player's center and end over the hitpoint. If it hits anything, then the ledge is 178 | #obstructed 179 | 180 | #In case of a ledge surface that slopes upwards going towards the player, I need to account for the rise 181 | #of the ledge to make sure the obstruction ray goes just above the edge. That's what I calculate this riseVec for 182 | Yangle = normal.angle(axisY) 183 | tan = -math.tan(Yangle - math.pi/2) 184 | run = (C.MaxRayDistance-C.MinRayDistance)/(C.NumRays-1) 185 | #The vertical rise over the ray step distance stepping TOWARDS PLAYER: 186 | rise = max(tan+.1, .1) * run 187 | 188 | riseVec = rise * axisZ 189 | 190 | #add riseVec to hitpoint to get the obstruction ray's ending pos 191 | pos2 = ledgeRay[1] + riseVec 192 | #subtract some junk from ending pos to get a starting position above the player's horizontal center 193 | pos1 = Vector(pos2) - (C.MinRayDistance*axisY + Vector(rayStepVec)*i) 194 | #stash the ray endpoints (plus color [0,1,1], that is, cyan) in a variable to be drawn further down (for testing) 195 | own['testvar'] = (pos1, pos2, [0,1,1]) 196 | obstructionRay = own.rayCast(pos2, pos1, 0, "", 1, 1) #Cast the ray 197 | if obstructionRay[0] == None: #If the ray found no obstruction... 198 | #Wall Raycasting 199 | #Now we want to shoot a ray to detect the wall of the ledge, to determine 200 | #the exact direction and position to put the player at while hanging 201 | #So we want this ray to go just under the edge 202 | 203 | #In case of ledges that slope downwards toward the player, I need to account for the fall of 204 | #the ledge to make sure the ray goes just under the ledge. VERY similar to calculating riseVec, above. 205 | fall = min((tan-.1),-.1) * run 206 | fallVec = fall * axisZ 207 | 208 | #Same things I did for the last raycast (this one is basically the same as the last, 209 | #except the ray is positioned a little lower) 210 | pos2 = ledgeRay[1] + fallVec 211 | pos1 = Vector(pos2) - (C.MinRayDistance*axisY + Vector(rayStepVec)*i) 212 | 213 | own['testvar2'] = (pos1, pos2, [0,1,1]) #Stash the ray endpoints in variable to be drawn further down 214 | wallRay = own.rayCast(pos2, pos1, 0, "", 1, 1) #Do raycast 215 | 216 | if wallRay[0] != None: #If the wall way found a wall... 217 | justGrabbed = True #finally set justGrabbed to true, to signal the transition into grab state 218 | ledgeVelocity = mathutils.Vector(ledgeRay[0].getLinearVelocity(0)) #get ledge velocity 219 | 220 | own['PrevLedgeVel'] = ledgeVelocity #store it in object property 221 | 222 | #Save raycast results and risevec data in object properties to be used during ledge grab/climb process 223 | ledgeRay = list(ledgeRay) 224 | ledgeRay[1] += ledgeVelocity/60 225 | wallRay = list(wallRay) 226 | wallRay[1] += ledgeVelocity/60 227 | own['LedgeRay'] = ledgeRay 228 | own['RiseVec'] = riseVec 229 | own['WallRay'] = wallRay 230 | 231 | break #break if one of the ledge rays has hit a ledge with an angle shallow enough to grab 232 | 233 | #Now I need to apply the ray step vecs to the start and end points for the next ray in the loop 234 | pos1 = [pos1[i] + rayStepVec[i] for i in range(3)] 235 | pos2 = [pos2[i] + rayStepVec2[i] for i in range(3)] 236 | 237 | #Uncomment these lines for test visualization of obstruction and wall rays 238 | #if 'testvar' in own: bge.render.drawLine(*own['testvar']) 239 | #if 'testvar2' in own: bge.render.drawLine(*own['testvar2']) 240 | 241 | ###Checkpoint Collision Test### 242 | #If a checkpoint is touched, store it in the 'RespawnObj' property 243 | checkSense = cont.sensors['Checkpoint'] 244 | if checkSense.positive: 245 | checkpoint = checkSense.hitObjectList[0] 246 | own['RespawnObj'] = checkpoint 247 | 248 | ###Death Collision Test### 249 | justDied = False 250 | deathSense = cont.sensors['Death'] 251 | #if death collision sensor or floor raycast detect an object with property 'Death'... 252 | if deathSense.positive or (ray[0] != None and 'Death' in ray[0]): 253 | justDied = True #You just died! 254 | 255 | ###Changing State### 256 | initializeClimb = False 257 | noFallBlendin = False 258 | 259 | if state == 'dead': 260 | if own['RespawnCount'] >= C.DeathDelay*60: #if time to respawn... 261 | if C.ResetSceneUponDeath == True: 262 | #Stash respawn object's position and orientation 263 | GD['PlayerSpawnPosition'] = Vector(own['RespawnObj'].worldPosition) 264 | GD['PlayerSpawnOrientation'] = mathutils.Matrix(own['RespawnObj'].worldOrientation) 265 | GD['SpawnPositionScene'] = sce.name 266 | sce.restart() 267 | else: 268 | state = 'fall' 269 | 270 | #If own['RespawnObj'] is not None (it would only be None in the corner case where 271 | #ResetSceneUponDeath was set to True, the player died and respawned, and thus 272 | #respawnObj would be set to None, THEN the user of this template changed 273 | #ResetSceneUponDeath to false via a script. 274 | 275 | if own['RespawnObj'] != None: 276 | #copy the respawn point's position and orientation 277 | own.worldPosition = own['RespawnObj'].worldPosition 278 | axisX, axisY, axisZ = own['RespawnObj'].worldOrientation.col 279 | #If spawn obj is None, respawn to position and orientation stashed in GlobalDict 280 | else: 281 | own.worldPosition = GD['PlayerSpawnPosition'] 282 | axisX, axisY, axisZ = GD['PlayerSpawnOrientation'].col 283 | own['RespawnCount'] = 0 #reset respawn counter to 0 284 | noFallBlendin = True #Don't blend into fall anim, transition IMMEDIATELY 285 | elif justDied == True: 286 | state = 'dead' 287 | elif justGrabbed == True: 288 | state = 'grab' 289 | elif state == 'climb': 290 | pass #Do nothing... this is just to escape from the elif chain 291 | #If player's feet touch the ground while grabbing a ledge... 292 | elif state == 'grab': 293 | if ray[0] != None: 294 | initializeClimb = True #climb immediately 295 | elif state == 'hanging': 296 | #Climb up if jump button is pushed 297 | if justJumped == True: 298 | initializeClimb = True 299 | #Drop down if drop button is pushed 300 | elif drop == True: 301 | state = 'fall' 302 | #Temporarily disable grab (will be re-enabled when player lands on floor again) 303 | own['NoMoreGrab'] = True 304 | #This has to do with the grabbing and climbing process; see relevant sections 305 | own['PrevOffset'] = None 306 | elif justJumped == True: 307 | state = 'jump' 308 | #During jump or spring state, when player is no longer moving upward, switch to fall state. 309 | elif state == 'jump' or state == 'spring': 310 | if own.getLinearVelocity(1)[2] <= 0: 311 | state = 'fall' 312 | #When not on floor, fall 313 | elif ray[0] == None or noStand == True: 314 | state = 'fall' 315 | #When standing on a spring surface... 316 | elif 'Spring' in ray[0] and (state == 'walk' or state == 'idle'): 317 | justJumped = True 318 | state = 'spring' 319 | 320 | elif isMoving == True: 321 | #If on ground, allow the player to grab again (if grab was disabled in the first place) 322 | own['NoMoreGrab'] = False 323 | state = 'walk' 324 | else: 325 | #If on ground, allow the player to grab again 326 | own['NoMoreGrab'] = False 327 | state = 'idle' 328 | 329 | #Climb state initialization 330 | if initializeClimb == True: 331 | state = 'climb' 332 | #Climbing has two phases (it always starts with phase 1): 333 | #Phase 1: the player (physics object) moves upwards until higher than ledge 334 | #Phase 2: player moves forward, over and onto ledge 335 | own['ClimbPhase'] = 1 336 | own['PrevOffset'] = None #Used during climbing/hanging; see relevant sections 337 | 338 | ###Calculating The Movement Direction Vector### 339 | 340 | #If the player is inputting a direction 341 | if isMoving == True: 342 | #Convert the input vector to an angle(in radians) 343 | inputAngle = math.atan2(inputVec[1], inputVec[0]) 344 | #Rotate angle so that the vector (0,1) maps to the angle 0 345 | inputAngle = inputAngle - (math.pi/2) 346 | 347 | #Rotate the forward direction vector by the inputAngle 348 | rotMat = mathutils.Matrix.Rotation(inputAngle, 3, axisZ) 349 | directionVec = rotMat * forwardAxis 350 | 351 | ###Movement### 352 | 353 | noIdleBlendin = False 354 | 355 | ##Movement when dead 356 | 357 | if state == 'dead': 358 | own['RespawnCount'] += 1 359 | #If not on standable ground, fall according to gravity and don't play death anim 360 | if ray[0] == None or noStand == True: 361 | playDeathAnim = False 362 | Xspeed, Yspeed, Zspeed = own.getLinearVelocity(1) 363 | Zspeed -= C.Gravity 364 | if Zspeed < -C.TerminalVelocity: 365 | Zspeed = -C.TerminalVelocity 366 | #If on standable ground, copy platform's velocity and play death anim 367 | else: 368 | playDeathAnim = True 369 | platformVelocity = mathutils.Vector(ray[0].getLinearVelocity(0)) * own.worldOrientation 370 | Xspeed, Yspeed, Zspeed = platformVelocity 371 | 372 | ##Movement during ledge hang, grab, climb 373 | 374 | elif state == 'hanging' or state == 'grab' or state == 'climb': 375 | 376 | #I use this function to move the player towards(but not past) desired positions at a certain speed during ledge grab/climb process 377 | #It operates on floats, not vectors(I apply it to eaxh axis individually). It returns a velocity 378 | def moveCloser(offset, speed): 379 | offset *= 60 380 | #if 'offset' is small enough to be reached in one frame traveling at given 'speed', then... 381 | if abs(offset) <= abs(speed): 382 | #We've traveled the entire offset, and are now on target 383 | onTarget = True 384 | #Return the velocity necessary to reach the offset in one frame (offset *60) 385 | outSpeed = offset 386 | #if 'offset' is too large to reach in one frame traveling at given 'speed', then... 387 | else: 388 | #We couldn't travel the entire offset, and so are not on target 389 | onTarget = False 390 | #Return a velocity equal to speed, but in the direction of the offset (negative if offset = negative) 391 | if offset < 0: outSpeed = -speed 392 | else: outSpeed = speed 393 | #Return whether we're on target and what the velocity is 394 | return (onTarget, outSpeed) 395 | 396 | #Velocity of the grabbed surface, from the PREVIOUS FRAME (technically two frames ago; see below) 397 | prevLedgeVel = Vector(own['PrevLedgeVel']) 398 | #Current velocity of the grabbed surface (technically one frame ago) 399 | #(Note that whenever you get the velocity of an object, it will actually be behind one frame) 400 | ledgeVelocity = Vector(ledgeRay[0].getLinearVelocity(0)) 401 | #Stash the current-frame velocity in own['PrevLedgeVel'] to use next frame 402 | own['PrevLedgeVel'] = Vector(ledgeVelocity) 403 | 404 | #This modifies the working velocity to compensate for the 1-frame lag mentioned above 405 | ledgeVelocity += ledgeVelocity - prevLedgeVel 406 | 407 | #Movement during climb 408 | if state == 'climb': 409 | #Stores whether or not player has reached target position (e.g. the climb is finished) (set false by default) 410 | inPosition = False 411 | #Stores the frame to start the climb animation on (set to 0 by default) 412 | climbStartFrame = 0 413 | #Move the ledge ray hit position according to ledge velocity, to sync with ledge movement 414 | ledgeRay[1] += ledgeVelocity/60 415 | #Move the player's position according to ledge velocity, to sync with ledge 416 | #Note that I'm only changing a COPY of the player's position. I update his actual position by applying velocity. 417 | pos = Vector(pos) + ledgeVelocity/60 418 | 419 | #The target point to climb towards (above and over the ledge) 420 | targetPoint = ledgeRay[1] + riseVec + C.Height * axisZ + C.ClimbLength*axisY 421 | #The difference vector between the player position and the target position 422 | offset = targetPoint - pos 423 | 424 | #Finds the climb speed that will complete the climb in the number of frames specified by C.ClimbAnim.endFrame 425 | speed = (C.Height+C.HangHeight+C.ClimbLength+C.HangDistance)/(C.ClimbAnim.endFrame/60) 426 | 427 | Zspeed, Yspeed = 0, 0 #set to 0 by default 428 | 429 | #If in first phase of climbing (the going up part)... 430 | if own['ClimbPhase'] == 1: 431 | #Find scalar offset along player's local Z axis from player to target point (projection of offset onto local Z axis) 432 | Zoffs = axisZ.dot(offset) 433 | #Find the Z velocity necessary to move towards (but not past) a point level with 434 | #the target point at given speed (Zspeed). Also find whether moving at that speed will bring us 435 | #vertically level with target point by next frame (onTarget). 436 | onTarget, Zspeed = moveCloser(Zoffs, speed) 437 | #If vertically aligned with target point, move to phase 2 of climb (the going forwards part) 438 | if onTarget: own['ClimbPhase'] = 2 439 | 440 | #Scalar offset along local Y axis (see Zoffs) 441 | Yoffs = axisY.dot(offset) 442 | #If in the second phase of climbing(moving forward)... 443 | if own['ClimbPhase'] == 2: 444 | #Get the Yspeed necessary to move towards target point, and whether the climb is finished (inPosition) 445 | inPosition, Yspeed = moveCloser(Yoffs, speed) 446 | 447 | #If on the first frame of the climb... 448 | if initializeClimb == True: 449 | #Determine how large the offset to TargetPoint relative to expected value when climbing out of a hang 450 | offsetRatio = (abs(Zoffs)+abs(Yoffs))/(C.Height+C.HangHeight+C.ClimbLength+C.HangDistance) 451 | #If offset is smaller than expected offset.. 452 | #(e.g. the player's feet touched the ground while grabbing and forced him into a climb before reaching hang state) 453 | if offsetRatio < 1: 454 | #Determine the climb anim start frame based on offset ratio (if offset is 1/2 usual value, climb anim will start halfway through) 455 | climbStartFrame = (1-offsetRatio)*C.ClimbAnim.endFrame 456 | #Snap the armature(NOT collision mesh) into hanging position 457 | armaturePosition = ledgeRay[1] - C.HangDistance*axisY -C.HangHeight*axisZ 458 | sce.objects[C.Armature].worldPosition = armaturePosition #assign the position 459 | #Parent the armature to the ledge object, removing parent to collision mesh in the process 460 | #(the climb is animated entirely through an action, so armature should be fixed in place) 461 | sce.objects[C.Armature].setParent(ledgeRay[0], False, False) 462 | 463 | #Transform ledgeVelcity into player local space, and apply to player's local linear velocity, so he moves with ledge 464 | ledgeVelocity = ledgeVelocity * own.worldOrientation 465 | Xspeed = ledgeVelocity[0] 466 | Yspeed += ledgeVelocity[1] 467 | Zspeed += ledgeVelocity[2] 468 | 469 | #If current offset is greater than or equal to than previous offset, something must be in the way, so we bail out of the climb 470 | bailout = False 471 | if own['PrevOffset'] != None and offset.length >= own['PrevOffset'].length: 472 | bailout = True 473 | 474 | #If the climb is finished (either properly or by bailout)... 475 | if inPosition or bailout: 476 | ###MID-CODE STATE CHANGE!!!### 477 | state = 'idle' 478 | own['PrevOffset'] = None #reset 'PrevOffset' to None 479 | #Set armature position to collision mesh position, and re-parent it to collision mesh 480 | sce.objects[C.Armature].worldPosition = own.worldPosition 481 | sce.objects[C.Armature].setParent(own, False, False) 482 | #Transition into idle animation must happen immediately, because the climbing animation offsets the entire skeleton 483 | noIdleBlendin = True 484 | 485 | #Assign current offset to 'PrevOffset' to use next frame (only if still in climb state) 486 | if state == 'climb': own['PrevOffset'] = offset 487 | 488 | #Movement while hanging 489 | if state == 'hanging': 490 | #Update the ray hit positions based on the ledge velocity 491 | ledgeRay[1] += ledgeVelocity/60 492 | wallRay[1] += ledgeVelocity/60 493 | 494 | #Update (a copy of) player position based on ledge velocity 495 | pos = Vector(pos) + ledgeVelocity/60 496 | #Calculate the difference vector between player position and target hanging position 497 | offset = wallRay[1] - pos + C.HangDistance*-axisY + C.HangHeight *-axisZ 498 | 499 | #If offset is greater than o equal to offset from last frame (e.g. something's in the way), and offset is greater than 500 | #tolerance, cancel the hand and go into fall state. 501 | if offset.length >= own['PrevOffset'].length and offset.length > C.GrabTolerance: 502 | ###MID-CODE STATE CHANGE!!!### 503 | state = 'fall' 504 | own['NoMoreGrab'] = True 505 | own['PrevOffset'] = None #Reset PrevOffset to None 506 | 507 | #if still in hang state, set PrevOffset to offset to be used next frame. 508 | if state == 'hanging': own['PrevOffset'] = offset 509 | 510 | #Transform ledge velocity to player-local space and apply it to player velocity. 511 | ledgeVelocity = ledgeVelocity * own.worldOrientation 512 | Xspeed, Yspeed, Zspeed = ledgeVelocity 513 | 514 | #Movement while grabbing a ledge 515 | elif state == 'grab': 516 | 517 | ###Parts of this are extremely similar to the 'climb' state section above. Look there for more comments. 518 | inPosition = True 519 | ledgeRay[1] += ledgeVelocity/60 520 | wallRay[1] += ledgeVelocity/60 521 | 522 | pos = Vector(pos) + ledgeVelocity/60 523 | 524 | normal = Vector(ledgeRay[2]) 525 | hitPoint = Vector(ledgeRay[1]) 526 | 527 | #Get the difference vector between current position target hang position 528 | offset = hitPoint - (pos + axisZ * C.HangHeight) 529 | 530 | minSpeed = C.MinGrabSpeed 531 | 532 | #Transform ledge velocity to player local space. 533 | ledgeVelocity = ledgeVelocity * own.worldOrientation 534 | #Get player Z velocity relaive to ledge velocity 535 | currentSpeed = own.getLinearVelocity(1)[2] - ledgeVelocity[2] 536 | 537 | #Set speed to abs(currentSpeed) unless current speed is less than minSpeed, in which case use minspeed. 538 | if abs(currentSpeed) > abs(minSpeed): speed = abs(currentSpeed) 539 | else: speed = minSpeed 540 | 541 | #Move closer to target along Z axis 542 | Zoffs = axisZ.dot(offset) 543 | onTarget, Zspeed = moveCloser(Zoffs, speed) 544 | inPosition = (onTarget and inPosition) 545 | ### 546 | 547 | #Get the normal of the ledge wall 548 | wallNorm = wallRay[2] 549 | #Align player Y axis with wall normal 550 | axisY = -TMath.VecToPlane(wallNorm, axisX, axisY).normalized() 551 | 552 | offset = wallRay[1] - pos + C.HangDistance*-axisY + C.HangHeight *-axisZ 553 | 554 | #bge.render.drawLine(pos, Vector(pos)+offset, [0,1,1]) 555 | 556 | HGS = C.HorizontalGrabSpeed 557 | 558 | #Move closer to target along X axis 559 | Xoffs = axisX.dot(offset) 560 | onTarget, Xspeed = moveCloser(Xoffs, HGS) 561 | inPosition = (onTarget and inPosition) 562 | 563 | #And Y axis 564 | Yoffs = axisY.dot(offset) 565 | onTarget, Yspeed = moveCloser(Yoffs, HGS) 566 | inPosition = (onTarget and inPosition) 567 | 568 | #Add ledge velocity to player velocity. 569 | Xspeed += ledgeVelocity[0] 570 | Yspeed += ledgeVelocity[1] 571 | Zspeed += ledgeVelocity[2] 572 | 573 | ###MID-CODE STATE CHANGE!!!### 574 | #If player is at target hang position, switch to hang state 575 | if inPosition == True: 576 | state = 'hanging' 577 | 578 | if own['PrevOffset'] != None: 579 | #If current offset is >= to previous (e.g. something is in the way)... 580 | if offset.length >= own['PrevOffset'].length: 581 | #If current offset is not within the range of tolerance, cancel the grab and switch to fall state 582 | if offset.length > C.GrabTolerance: 583 | state = 'fall' 584 | own['NoMoreGrab'] = True 585 | Xspeed, Yspeed = 0,0 586 | own['PrevOffset'] = None 587 | #If current offset is within tolerance, just go to hang state. 588 | else: 589 | state = 'hanging' 590 | 591 | #Set PrevOffset to offset, to be used next frame (but NOT if state has been changed to fall) 592 | if state == 'grab' or state == 'hanging': own['PrevOffset'] = offset 593 | 594 | #if not in grab state... 595 | else: 596 | ##Air Control and Falling## 597 | if state == 'fall' or state == 'jump' or state == 'spring': 598 | #Get the velocities from last frame 599 | Xspeed, Yspeed, Zspeed = own.getLinearVelocity(1) 600 | #If human player is inputting a direction... 601 | if isMoving == True: 602 | #Project the input vector onto player's Y-axis to get local Y component of input 603 | Yair = directionVec.dot(axisY) 604 | #Apply acceleration in appropriate direction, ONLY IF Yspeed is below the maximum 605 | #for forward or backwards movement 606 | if Yair >= 0 and Yspeed < C.AirMaxFront: 607 | Yspeed += Yair * C.AirAccelFront 608 | elif Yair < 0 and Yspeed > -C.AirMaxFront: 609 | Yspeed += Yair * C.AirAccelBack 610 | #Do the same for X axis, but since there is only one max speed for sideways movement, 611 | #only one check is needed. 612 | Xair = directionVec.dot(axisX) 613 | if (Xair >= 0 and Xspeed < C.AirMaxSide) or (Xair < 0 and Xspeed > -C.AirMaxSide): 614 | Xspeed += Xair * C.AirAccelSide 615 | 616 | gravity = C.Gravity #Get gravity 617 | #If in jump state, and jump button is not held down, multiply gravity by short jump factor 618 | if state == 'jump' and heldJump == False: 619 | gravity /= C.ShortJump 620 | 621 | #Falling: apply gravity, then if abs Zspeed exceeds terminal velocity, set it to terminal velocity. 622 | Zspeed -= gravity 623 | if Zspeed < -C.TerminalVelocity: 624 | Zspeed = -C.TerminalVelocity 625 | 626 | else: 627 | ##Calculating the target speed 628 | #Normal of the ground surface that the ray hit. 629 | groundNormal = ray[2] 630 | 631 | if groundNormal.dot(axisZ) < 0: 632 | groundNormal *= -1 633 | 634 | #The angle between the player's local Y-axis and the ground normal 635 | angle = axisY.angle(groundNormal) 636 | #The sine of that angle 637 | sine = math.sin(angle) 638 | 639 | factorUp = C.UphillSlopeFac 640 | factorDown = C.DownhillSlopeFac 641 | 642 | ##Calculate the slope factor(that will modulate the player's speed on slopes) 643 | #If the player is going uphill... 644 | if angle > math.pi/2: 645 | #This algorithm is complex, but isn't based on real physics, I just wanted something that 'felt right' 646 | 647 | #Ground normal flattened onto plane defined by player's local horizontal axes 648 | flatNormal = TMath.VecToPlane(groundNormal, axisX, axisY) 649 | #Try to find angle between local Y (forward) axis and flatNormal. If it fails, go with 90 degrees. 650 | try: flatAngle = axisY.angle(flatNormal) 651 | except: flatAngle = math.pi/2 652 | #Sine and cosine of this angle 653 | flatSine = math.sin(flatAngle) 654 | flatCosine = math.cos(flatAngle) 655 | #The absolute angle of the slope 656 | slopeAngle = axisZ.angle(groundNormal) 657 | #Don't quite remember how this part works... 658 | interp = (slopeAngle/(math.pi/2))**(1/(factorUp+.0000001)) 659 | thisLimit = 1-interp 660 | 661 | ratio = max(min(thisLimit / abs(flatCosine),1),0) 662 | slopeFactor = ratio 663 | #If the player is going downhill... 664 | else: 665 | #Another algorithm created through trial-and-error... 666 | slopeFactor = (sine+factorDown*(1-sine))/sine 667 | 668 | #Since the player's speed is always applied along his forward y-axis(not along the slope direction), 669 | #he would end up going faster when on any slope... This factor corrects for that. 670 | correctionFactor = sine 671 | 672 | #Get the speed modifier of the ground object 673 | try: speedMod = ray[0]['SpeedModifier'] 674 | except: speedMod = 1 675 | 676 | #The final target speed 677 | targetSpd = maxSpd * inputVec.length * correctionFactor * slopeFactor * speedMod 678 | ################################ 679 | 680 | ###Now to change the player's direction and Yspeed### 681 | #Subtract the Y platform velocity from the player's total velocity to get the Y velocity RELATIVE to the platform 682 | #All velocities are LOCAL to the player's orientation 683 | Yspeed = own.getLinearVelocity(1)[1] - platformVelocity[1] 684 | 685 | if isMoving == True: 686 | #compensate for turning lag when original and new direction vectors are opposite(or close to it) 687 | if (axisY + directionVec).length <= 0.1: 688 | axisY += axisX * 0.1 689 | #blend old and new vectors according to smoothness factor(thus smoothing the turning) 690 | axisY = (1 - smooth) * directionVec + smooth * axisY 691 | axisY.normalize() 692 | #increase speed according to acceleration 693 | Yspeed = Yspeed + maxSpd * C.Acceleration 694 | if Yspeed > targetSpd: 695 | Yspeed = targetSpd 696 | else: 697 | #decrease speed according to deceleration 698 | Yspeed = Yspeed - maxSpd * C.Deceleration 699 | if Yspeed < 0: 700 | Yspeed = 0 701 | #Set X velocity to 0 (we only want him moving forward 702 | Xspeed = 0 703 | 704 | ###Latching Onto Ground, Jumping### 705 | 706 | #If a jump has just been activated, push the player upwards 707 | if justJumped == True: 708 | if state == 'spring': 709 | Zspeed += ray[0]['Spring'] 710 | else: 711 | Zspeed += C.Jump 712 | 713 | #If player is not in the air, snap the player onto the ground and set Z velocity to 0 714 | if state != 'fall' and state != 'jump' and state != 'spring': 715 | #Find the position the player should be snapped to 716 | rayHitPos = ray[1] 717 | rayHitPos = [rayHitPos[i] + C.Height * axisZ[i] for i in range(3)] 718 | #Get the current-frame velocity of the platform the player's standing on, and transform it to PLAYER LOCAL space 719 | platformVelocity = list(mathutils.Vector(ray[0].getLinearVelocity(0)) * own.worldOrientation ) 720 | #When the ground is flat, smoothly blend instead of snap (to allow smooth movement on stairs) 721 | if sine >= 0.9999: 722 | stepSmooth = C.StepSmoothing 723 | if stepSmooth < 0.01: 724 | stepSmooth = .01 725 | oldPosition = mathutils.Vector(own.worldPosition) 726 | positionalDif = mathutils.Vector(rayHitPos)-oldPosition 727 | if positionalDif.length > (1.1/(60*stepSmooth))*abs(Yspeed): 728 | own.worldPosition = list(oldPosition + (1.1/(60*stepSmooth))*positionalDif.normalized()*abs(Yspeed)) 729 | else: own.worldPosition = rayHitPos 730 | #When the ground is sloped, snap to it 731 | else: 732 | own.worldPosition = rayHitPos 733 | #Set the player's Z velocity to the platform's Z velocity 734 | Zspeed = platformVelocity[2] 735 | #Add the platform's XY velocities to the player's relative velocities to get the player's absolute (Local) XY velocities 736 | Yspeed += platformVelocity[1] 737 | Xspeed += platformVelocity[0] 738 | 739 | 740 | ###Setting Animations### 741 | 742 | #Get the armature 743 | arm = sce.objects[C.Armature] 744 | 745 | #Grab some functions from the animation helper module (See Animation.py module for documentation) 746 | #This function sets an animation to play for a given armature, on a given layer 747 | setAnim = AnimationHelper.setLayerAnim 748 | #This function gets the last animation playing for a given armature, on a given layer 749 | getAnim = AnimationHelper.getLayerAnim 750 | 751 | #NOTE: "animations" are objects that contain an action to play, and a list of playing properties, such as speed, 752 | #last frame, etc. They have no methods, they are just bags of properties. 753 | #See the comments in the AnimationHelper.py module for documentation on the Animation class 754 | 755 | #Get all animations from config file 756 | animations = [C.DeathAnim, C.ClimbAnim, C.HangAnim, C.FallAnim, C.RunAnim, C.WalkAnim, C.IdleAnim] 757 | #Here I make copies of all the animations, so I can change their properties without permanently changing the settings 758 | #in the config file 759 | deathA,climbA,hangA,fallA,runA,walkA,idleA = [copy.copy(i) for i in animations] 760 | 761 | #Set animation properties 762 | if state == 'dead': 763 | if playDeathAnim == True: 764 | setAnim(arm, 0, deathA) #Set animation 765 | elif state == 'climb': 766 | #Set the climb animation entry frame to the one determined eariler in climb section. 767 | climbA.entryFrame = climbStartFrame 768 | setAnim(arm, 0, climbA) 769 | elif state == 'hanging' or state == 'grab': 770 | setAnim(arm, 0, hangA) 771 | elif state == 'fall' or state == 'jump' or state == 'spring': 772 | #If fall blendin is disabled, set fall animation blendin to zero (only used when respawning) 773 | if noFallBlendin: 774 | fallA.blendin = 0 775 | setAnim(arm, 0, fallA) 776 | else: 777 | if state == 'walk': 778 | #If speed is greater than walk speed (e.g. player is running)... 779 | if Yspeed > walkSpd*1.001: 780 | #If the currently playing anim is the walk anim... 781 | if getAnim(arm, 0).name == C.WalkAnim.name: 782 | #Set the run anim entry frame to the walk anim current frame, for continuity between animations 783 | runA.entryFrame = arm.getActionFrame(0) 784 | #Set playspeed to RunAnim.playspeed times ratio of current speed to max speed. 785 | runA.playSpeed = C.RunAnim.playSpeed*Yspeed/maxSpd 786 | setAnim(arm, 0, runA) 787 | #If player is walking... 788 | else: 789 | #Similar to run anim 790 | if getAnim(arm, 0).name == C.RunAnim.name: 791 | walkA.entryFrame = arm.getActionFrame(0) 792 | walkA.playSpeed = C.WalkAnim.playSpeed*Yspeed/walkSpd 793 | setAnim(arm, 0, walkA) 794 | 795 | elif state == 'idle': 796 | #If idle blendin is disabled, set idle anim blendin to zero (used after climbing ledge) 797 | if noIdleBlendin: 798 | idleA.blendin = 0 799 | setAnim(arm, 0, idleA) 800 | 801 | 802 | AnimationHelper.manageAnims(arm) #Once again, see AnimationHelper.py for documentation 803 | 804 | ###Final Stuff### 805 | #Make sure the X axis is orthogonal to the other two 806 | axisX = axisY.cross(axisZ) 807 | own.worldOrientation = TMath.MakeMatrix(axisX,axisY,axisZ) 808 | own.setLinearVelocity([Xspeed,Yspeed,Zspeed], 1) 809 | own['State'] = state 810 | #Update the platform velocity variable 811 | own['PlatformVelocity'] = platformVelocity 812 | #Negate global gravity 813 | own.applyForce(-sce.gravity * own.mass) -------------------------------------------------------------------------------- /red_maze_blue_maze/TMath.py: -------------------------------------------------------------------------------- 1 | #This module just contains some convenient 3D geometric math functions. 2 | #Why did I call it TMath? Well, it started off as a bunch of functions to perform vector 3 | #math operations on tuples (t for tuple). Then I discovered mathutils Vector class. 4 | #I kept the name for the historical significance. 5 | 6 | import math 7 | import mathutils 8 | 9 | #Project(flatten) 3D vector 'a' onto the plane containing vectors 'x' and 'y' (normalizes the result) 10 | def VecToPlane(a,x,y): 11 | n = x.cross(y) 12 | n.normalize() 13 | final = a - (a.dot(n) * n) 14 | final.normalize() 15 | return final 16 | 17 | #Compose an orientation matrix from X, Y, and Z axes 18 | def MakeMatrix(x,y,z): 19 | final = [[],[],[]] 20 | for i in range(len(final)): 21 | final[i] = [x[i], y[i], z[i]] 22 | return final 23 | 24 | #Interpolate rotation matrices 'a' and 'b' according to factor 'c' 25 | #This method will interpolate from one orientation to another using the LEAST ROTATION possible 26 | #Did I cheat by using the mathutils quaternion class? :) 27 | def MatrixLerp(a,b,c): 28 | a = a.to_quaternion() 29 | b = b.to_quaternion() 30 | a = a.slerp(b,c) 31 | a = a.to_matrix() 32 | return a 33 | 34 | #Spherically interpolate 3D vectors 'a' and 'b' by factor 'c'(not fully tested) 35 | def VecSlerp(a,b,c): 36 | angle = c * a.angle(b,0) 37 | axis = b.cross(a) 38 | if a.cross(axis).dot(b) < 0: 39 | angle = -angle 40 | rotmat = mathutils.Matrix.Rotation(angle, 3, axis) 41 | a = a * rotmat 42 | return a 43 | 44 | #Given two vectors, 'a' and 'b', generate a rotation matrix that maps 'a' onto 'b'(not thoroughly tested for all cases- may have errors) 45 | def VecToVecMatrix(a,b): 46 | angle = a.angle(b) 47 | perp = a.cross(b) 48 | if a.cross(perp).dot(b) < 0: 49 | angle = -angle 50 | rotmat = mathutils.Matrix.Rotation(angle, 3, perp) 51 | return rotmat 52 | -------------------------------------------------------------------------------- /red_maze_blue_maze/red_maze_blue_maze.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/mazes/d93906825212980398a5895cb21ea321cf1e06ab/red_maze_blue_maze/red_maze_blue_maze.blend -------------------------------------------------------------------------------- /red_maze_blue_maze/sky_blue_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/mazes/d93906825212980398a5895cb21ea321cf1e06ab/red_maze_blue_maze/sky_blue_02.png -------------------------------------------------------------------------------- /steely_taws_2d_maze.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/mazes/d93906825212980398a5895cb21ea321cf1e06ab/steely_taws_2d_maze.blend -------------------------------------------------------------------------------- /steely_taws_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/mazes/d93906825212980398a5895cb21ea321cf1e06ab/steely_taws_screenshot.png --------------------------------------------------------------------------------