├── texture.png ├── LICENSE ├── README.md └── main.py /texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/Minecraft/master/texture.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Michael Fogleman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minecraft 2 | 3 | Simple Minecraft-inspired demo written in Python and Pyglet. 4 | 5 | http://www.youtube.com/watch?v=kC3lwK631X8 6 | 7 | **Like this project?** 8 | 9 | You might also like my other Minecraft clone written in C using modern OpenGL (GL shader language). It performs better, has better terrain generation and saves state to a sqlite database. See here: 10 | 11 | https://github.com/fogleman/Craft 12 | 13 | ## Goals and Vision 14 | 15 | I would like to see this project turn into an educational tool. Kids love Minecraft and Python is a great first language. 16 | This is a good opportunity to get children excited about programming. 17 | 18 | The code should become well commented and more easily configurable. It should be easy to make some simple changes 19 | and see the results quickly. 20 | 21 | I think it would be great to turn the project into more of a library / API... a Python package that you import and then 22 | use / configure to setup a world and run it. Something along these lines... 23 | 24 | 25 | ```python 26 | import mc 27 | 28 | world = mc.World(...) 29 | world.set_block(x, y, z, mc.DIRT) 30 | mc.run(world) 31 | ``` 32 | 33 | The API could contain functionality for the following: 34 | 35 | - Easily configurable parameters like gravity, jump velocity, walking speed, etc. 36 | - Hooks for terrain generation. 37 | 38 | ## How to Run 39 | 40 | ```shell 41 | pip install pyglet 42 | git clone https://github.com/fogleman/Minecraft.git 43 | cd Minecraft 44 | python main.py 45 | ``` 46 | 47 | ### Mac 48 | 49 | On Mac OS X, you may have an issue with running Pyglet in 64-bit mode. Try running Python in 32-bit mode first: 50 | 51 | ```shell 52 | arch -i386 python main.py 53 | ``` 54 | 55 | If that doesn't work, set Python to run in 32-bit mode by default: 56 | 57 | ```shell 58 | defaults write com.apple.versioner.python Prefer-32-Bit -bool yes 59 | ``` 60 | 61 | This assumes you are using the OS X default Python. Works on Lion 10.7 with the default Python 2.7, and may work on other versions too. Please raise an issue if not. 62 | 63 | Or try Pyglet 1.2 alpha, which supports 64-bit mode: 64 | 65 | ```shell 66 | pip install https://pyglet.googlecode.com/files/pyglet-1.2alpha1.tar.gz 67 | ``` 68 | 69 | ### If you don't have pip or git 70 | 71 | For pip: 72 | 73 | - Mac or Linux: install with `sudo easy_install pip` (Mac or Linux) - or (Linux) find a package called something like 'python-pip' in your package manager. 74 | - Windows: [install Distribute then Pip](http://stackoverflow.com/a/12476379/992887) using the linked .MSI installers. 75 | 76 | For git: 77 | 78 | - Mac: install [Homebrew](http://mxcl.github.com/homebrew/) first, then `brew install git`. 79 | - Windows or Linux: see [Installing Git](http://git-scm.com/book/en/Getting-Started-Installing-Git) from the _Pro Git_ book. 80 | 81 | See the [wiki](https://github.com/fogleman/Minecraft/wiki) for this project to install Python, and other tips. 82 | 83 | ## How to Play 84 | 85 | ### Moving 86 | 87 | - W: forward 88 | - S: back 89 | - A: strafe left 90 | - D: strafe right 91 | - Mouse: look around 92 | - Space: jump 93 | - Tab: toggle flying mode 94 | 95 | ### Building 96 | 97 | - Selecting type of block to create: 98 | - 1: brick 99 | - 2: grass 100 | - 3: sand 101 | - Mouse left-click: remove block 102 | - Mouse right-click: create block 103 | 104 | ### Quitting 105 | 106 | - ESC: release mouse, then close window 107 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import sys 4 | import math 5 | import random 6 | import time 7 | 8 | from collections import deque 9 | from pyglet import image 10 | from pyglet.gl import * 11 | from pyglet.graphics import TextureGroup 12 | from pyglet.window import key, mouse 13 | 14 | TICKS_PER_SEC = 60 15 | 16 | # Size of sectors used to ease block loading. 17 | SECTOR_SIZE = 16 18 | 19 | WALKING_SPEED = 5 20 | FLYING_SPEED = 15 21 | 22 | GRAVITY = 20.0 23 | MAX_JUMP_HEIGHT = 1.0 # About the height of a block. 24 | # To derive the formula for calculating jump speed, first solve 25 | # v_t = v_0 + a * t 26 | # for the time at which you achieve maximum height, where a is the acceleration 27 | # due to gravity and v_t = 0. This gives: 28 | # t = - v_0 / a 29 | # Use t and the desired MAX_JUMP_HEIGHT to solve for v_0 (jump speed) in 30 | # s = s_0 + v_0 * t + (a * t^2) / 2 31 | JUMP_SPEED = math.sqrt(2 * GRAVITY * MAX_JUMP_HEIGHT) 32 | TERMINAL_VELOCITY = 50 33 | 34 | PLAYER_HEIGHT = 2 35 | 36 | if sys.version_info[0] >= 3: 37 | xrange = range 38 | 39 | def cube_vertices(x, y, z, n): 40 | """ Return the vertices of the cube at position x, y, z with size 2*n. 41 | 42 | """ 43 | return [ 44 | x-n,y+n,z-n, x-n,y+n,z+n, x+n,y+n,z+n, x+n,y+n,z-n, # top 45 | x-n,y-n,z-n, x+n,y-n,z-n, x+n,y-n,z+n, x-n,y-n,z+n, # bottom 46 | x-n,y-n,z-n, x-n,y-n,z+n, x-n,y+n,z+n, x-n,y+n,z-n, # left 47 | x+n,y-n,z+n, x+n,y-n,z-n, x+n,y+n,z-n, x+n,y+n,z+n, # right 48 | x-n,y-n,z+n, x+n,y-n,z+n, x+n,y+n,z+n, x-n,y+n,z+n, # front 49 | x+n,y-n,z-n, x-n,y-n,z-n, x-n,y+n,z-n, x+n,y+n,z-n, # back 50 | ] 51 | 52 | 53 | def tex_coord(x, y, n=4): 54 | """ Return the bounding vertices of the texture square. 55 | 56 | """ 57 | m = 1.0 / n 58 | dx = x * m 59 | dy = y * m 60 | return dx, dy, dx + m, dy, dx + m, dy + m, dx, dy + m 61 | 62 | 63 | def tex_coords(top, bottom, side): 64 | """ Return a list of the texture squares for the top, bottom and side. 65 | 66 | """ 67 | top = tex_coord(*top) 68 | bottom = tex_coord(*bottom) 69 | side = tex_coord(*side) 70 | result = [] 71 | result.extend(top) 72 | result.extend(bottom) 73 | result.extend(side * 4) 74 | return result 75 | 76 | 77 | TEXTURE_PATH = 'texture.png' 78 | 79 | GRASS = tex_coords((1, 0), (0, 1), (0, 0)) 80 | SAND = tex_coords((1, 1), (1, 1), (1, 1)) 81 | BRICK = tex_coords((2, 0), (2, 0), (2, 0)) 82 | STONE = tex_coords((2, 1), (2, 1), (2, 1)) 83 | 84 | FACES = [ 85 | ( 0, 1, 0), 86 | ( 0,-1, 0), 87 | (-1, 0, 0), 88 | ( 1, 0, 0), 89 | ( 0, 0, 1), 90 | ( 0, 0,-1), 91 | ] 92 | 93 | 94 | def normalize(position): 95 | """ Accepts `position` of arbitrary precision and returns the block 96 | containing that position. 97 | 98 | Parameters 99 | ---------- 100 | position : tuple of len 3 101 | 102 | Returns 103 | ------- 104 | block_position : tuple of ints of len 3 105 | 106 | """ 107 | x, y, z = position 108 | x, y, z = (int(round(x)), int(round(y)), int(round(z))) 109 | return (x, y, z) 110 | 111 | 112 | def sectorize(position): 113 | """ Returns a tuple representing the sector for the given `position`. 114 | 115 | Parameters 116 | ---------- 117 | position : tuple of len 3 118 | 119 | Returns 120 | ------- 121 | sector : tuple of len 3 122 | 123 | """ 124 | x, y, z = normalize(position) 125 | x, y, z = x // SECTOR_SIZE, y // SECTOR_SIZE, z // SECTOR_SIZE 126 | return (x, 0, z) 127 | 128 | 129 | class Model(object): 130 | 131 | def __init__(self): 132 | 133 | # A Batch is a collection of vertex lists for batched rendering. 134 | self.batch = pyglet.graphics.Batch() 135 | 136 | # A TextureGroup manages an OpenGL texture. 137 | self.group = TextureGroup(image.load(TEXTURE_PATH).get_texture()) 138 | 139 | # A mapping from position to the texture of the block at that position. 140 | # This defines all the blocks that are currently in the world. 141 | self.world = {} 142 | 143 | # Same mapping as `world` but only contains blocks that are shown. 144 | self.shown = {} 145 | 146 | # Mapping from position to a pyglet `VertextList` for all shown blocks. 147 | self._shown = {} 148 | 149 | # Mapping from sector to a list of positions inside that sector. 150 | self.sectors = {} 151 | 152 | # Simple function queue implementation. The queue is populated with 153 | # _show_block() and _hide_block() calls 154 | self.queue = deque() 155 | 156 | self._initialize() 157 | 158 | def _initialize(self): 159 | """ Initialize the world by placing all the blocks. 160 | 161 | """ 162 | n = 80 # 1/2 width and height of world 163 | s = 1 # step size 164 | y = 0 # initial y height 165 | for x in xrange(-n, n + 1, s): 166 | for z in xrange(-n, n + 1, s): 167 | # create a layer stone an grass everywhere. 168 | self.add_block((x, y - 2, z), GRASS, immediate=False) 169 | self.add_block((x, y - 3, z), STONE, immediate=False) 170 | if x in (-n, n) or z in (-n, n): 171 | # create outer walls. 172 | for dy in xrange(-2, 3): 173 | self.add_block((x, y + dy, z), STONE, immediate=False) 174 | 175 | # generate the hills randomly 176 | o = n - 10 177 | for _ in xrange(120): 178 | a = random.randint(-o, o) # x position of the hill 179 | b = random.randint(-o, o) # z position of the hill 180 | c = -1 # base of the hill 181 | h = random.randint(1, 6) # height of the hill 182 | s = random.randint(4, 8) # 2 * s is the side length of the hill 183 | d = 1 # how quickly to taper off the hills 184 | t = random.choice([GRASS, SAND, BRICK]) 185 | for y in xrange(c, c + h): 186 | for x in xrange(a - s, a + s + 1): 187 | for z in xrange(b - s, b + s + 1): 188 | if (x - a) ** 2 + (z - b) ** 2 > (s + 1) ** 2: 189 | continue 190 | if (x - 0) ** 2 + (z - 0) ** 2 < 5 ** 2: 191 | continue 192 | self.add_block((x, y, z), t, immediate=False) 193 | s -= d # decrement side length so hills taper off 194 | 195 | def hit_test(self, position, vector, max_distance=8): 196 | """ Line of sight search from current position. If a block is 197 | intersected it is returned, along with the block previously in the line 198 | of sight. If no block is found, return None, None. 199 | 200 | Parameters 201 | ---------- 202 | position : tuple of len 3 203 | The (x, y, z) position to check visibility from. 204 | vector : tuple of len 3 205 | The line of sight vector. 206 | max_distance : int 207 | How many blocks away to search for a hit. 208 | 209 | """ 210 | m = 8 211 | x, y, z = position 212 | dx, dy, dz = vector 213 | previous = None 214 | for _ in xrange(max_distance * m): 215 | key = normalize((x, y, z)) 216 | if key != previous and key in self.world: 217 | return key, previous 218 | previous = key 219 | x, y, z = x + dx / m, y + dy / m, z + dz / m 220 | return None, None 221 | 222 | def exposed(self, position): 223 | """ Returns False is given `position` is surrounded on all 6 sides by 224 | blocks, True otherwise. 225 | 226 | """ 227 | x, y, z = position 228 | for dx, dy, dz in FACES: 229 | if (x + dx, y + dy, z + dz) not in self.world: 230 | return True 231 | return False 232 | 233 | def add_block(self, position, texture, immediate=True): 234 | """ Add a block with the given `texture` and `position` to the world. 235 | 236 | Parameters 237 | ---------- 238 | position : tuple of len 3 239 | The (x, y, z) position of the block to add. 240 | texture : list of len 3 241 | The coordinates of the texture squares. Use `tex_coords()` to 242 | generate. 243 | immediate : bool 244 | Whether or not to draw the block immediately. 245 | 246 | """ 247 | if position in self.world: 248 | self.remove_block(position, immediate) 249 | self.world[position] = texture 250 | self.sectors.setdefault(sectorize(position), []).append(position) 251 | if immediate: 252 | if self.exposed(position): 253 | self.show_block(position) 254 | self.check_neighbors(position) 255 | 256 | def remove_block(self, position, immediate=True): 257 | """ Remove the block at the given `position`. 258 | 259 | Parameters 260 | ---------- 261 | position : tuple of len 3 262 | The (x, y, z) position of the block to remove. 263 | immediate : bool 264 | Whether or not to immediately remove block from canvas. 265 | 266 | """ 267 | del self.world[position] 268 | self.sectors[sectorize(position)].remove(position) 269 | if immediate: 270 | if position in self.shown: 271 | self.hide_block(position) 272 | self.check_neighbors(position) 273 | 274 | def check_neighbors(self, position): 275 | """ Check all blocks surrounding `position` and ensure their visual 276 | state is current. This means hiding blocks that are not exposed and 277 | ensuring that all exposed blocks are shown. Usually used after a block 278 | is added or removed. 279 | 280 | """ 281 | x, y, z = position 282 | for dx, dy, dz in FACES: 283 | key = (x + dx, y + dy, z + dz) 284 | if key not in self.world: 285 | continue 286 | if self.exposed(key): 287 | if key not in self.shown: 288 | self.show_block(key) 289 | else: 290 | if key in self.shown: 291 | self.hide_block(key) 292 | 293 | def show_block(self, position, immediate=True): 294 | """ Show the block at the given `position`. This method assumes the 295 | block has already been added with add_block() 296 | 297 | Parameters 298 | ---------- 299 | position : tuple of len 3 300 | The (x, y, z) position of the block to show. 301 | immediate : bool 302 | Whether or not to show the block immediately. 303 | 304 | """ 305 | texture = self.world[position] 306 | self.shown[position] = texture 307 | if immediate: 308 | self._show_block(position, texture) 309 | else: 310 | self._enqueue(self._show_block, position, texture) 311 | 312 | def _show_block(self, position, texture): 313 | """ Private implementation of the `show_block()` method. 314 | 315 | Parameters 316 | ---------- 317 | position : tuple of len 3 318 | The (x, y, z) position of the block to show. 319 | texture : list of len 3 320 | The coordinates of the texture squares. Use `tex_coords()` to 321 | generate. 322 | 323 | """ 324 | x, y, z = position 325 | vertex_data = cube_vertices(x, y, z, 0.5) 326 | texture_data = list(texture) 327 | # create vertex list 328 | # FIXME Maybe `add_indexed()` should be used instead 329 | self._shown[position] = self.batch.add(24, GL_QUADS, self.group, 330 | ('v3f/static', vertex_data), 331 | ('t2f/static', texture_data)) 332 | 333 | def hide_block(self, position, immediate=True): 334 | """ Hide the block at the given `position`. Hiding does not remove the 335 | block from the world. 336 | 337 | Parameters 338 | ---------- 339 | position : tuple of len 3 340 | The (x, y, z) position of the block to hide. 341 | immediate : bool 342 | Whether or not to immediately remove the block from the canvas. 343 | 344 | """ 345 | self.shown.pop(position) 346 | if immediate: 347 | self._hide_block(position) 348 | else: 349 | self._enqueue(self._hide_block, position) 350 | 351 | def _hide_block(self, position): 352 | """ Private implementation of the 'hide_block()` method. 353 | 354 | """ 355 | self._shown.pop(position).delete() 356 | 357 | def show_sector(self, sector): 358 | """ Ensure all blocks in the given sector that should be shown are 359 | drawn to the canvas. 360 | 361 | """ 362 | for position in self.sectors.get(sector, []): 363 | if position not in self.shown and self.exposed(position): 364 | self.show_block(position, False) 365 | 366 | def hide_sector(self, sector): 367 | """ Ensure all blocks in the given sector that should be hidden are 368 | removed from the canvas. 369 | 370 | """ 371 | for position in self.sectors.get(sector, []): 372 | if position in self.shown: 373 | self.hide_block(position, False) 374 | 375 | def change_sectors(self, before, after): 376 | """ Move from sector `before` to sector `after`. A sector is a 377 | contiguous x, y sub-region of world. Sectors are used to speed up 378 | world rendering. 379 | 380 | """ 381 | before_set = set() 382 | after_set = set() 383 | pad = 4 384 | for dx in xrange(-pad, pad + 1): 385 | for dy in [0]: # xrange(-pad, pad + 1): 386 | for dz in xrange(-pad, pad + 1): 387 | if dx ** 2 + dy ** 2 + dz ** 2 > (pad + 1) ** 2: 388 | continue 389 | if before: 390 | x, y, z = before 391 | before_set.add((x + dx, y + dy, z + dz)) 392 | if after: 393 | x, y, z = after 394 | after_set.add((x + dx, y + dy, z + dz)) 395 | show = after_set - before_set 396 | hide = before_set - after_set 397 | for sector in show: 398 | self.show_sector(sector) 399 | for sector in hide: 400 | self.hide_sector(sector) 401 | 402 | def _enqueue(self, func, *args): 403 | """ Add `func` to the internal queue. 404 | 405 | """ 406 | self.queue.append((func, args)) 407 | 408 | def _dequeue(self): 409 | """ Pop the top function from the internal queue and call it. 410 | 411 | """ 412 | func, args = self.queue.popleft() 413 | func(*args) 414 | 415 | def process_queue(self): 416 | """ Process the entire queue while taking periodic breaks. This allows 417 | the game loop to run smoothly. The queue contains calls to 418 | _show_block() and _hide_block() so this method should be called if 419 | add_block() or remove_block() was called with immediate=False 420 | 421 | """ 422 | start = time.perf_counter() 423 | while self.queue and time.perf_counter() - start < 1.0 / TICKS_PER_SEC: 424 | self._dequeue() 425 | 426 | def process_entire_queue(self): 427 | """ Process the entire queue with no breaks. 428 | 429 | """ 430 | while self.queue: 431 | self._dequeue() 432 | 433 | 434 | class Window(pyglet.window.Window): 435 | 436 | def __init__(self, *args, **kwargs): 437 | super(Window, self).__init__(*args, **kwargs) 438 | 439 | # Whether or not the window exclusively captures the mouse. 440 | self.exclusive = False 441 | 442 | # When flying gravity has no effect and speed is increased. 443 | self.flying = False 444 | 445 | # Strafing is moving lateral to the direction you are facing, 446 | # e.g. moving to the left or right while continuing to face forward. 447 | # 448 | # First element is -1 when moving forward, 1 when moving back, and 0 449 | # otherwise. The second element is -1 when moving left, 1 when moving 450 | # right, and 0 otherwise. 451 | self.strafe = [0, 0] 452 | 453 | # Current (x, y, z) position in the world, specified with floats. Note 454 | # that, perhaps unlike in math class, the y-axis is the vertical axis. 455 | self.position = (0, 0, 0) 456 | 457 | # First element is rotation of the player in the x-z plane (ground 458 | # plane) measured from the z-axis down. The second is the rotation 459 | # angle from the ground plane up. Rotation is in degrees. 460 | # 461 | # The vertical plane rotation ranges from -90 (looking straight down) to 462 | # 90 (looking straight up). The horizontal rotation range is unbounded. 463 | self.rotation = (0, 0) 464 | 465 | # Which sector the player is currently in. 466 | self.sector = None 467 | 468 | # The crosshairs at the center of the screen. 469 | self.reticle = None 470 | 471 | # Velocity in the y (upward) direction. 472 | self.dy = 0 473 | 474 | # A list of blocks the player can place. Hit num keys to cycle. 475 | self.inventory = [BRICK, GRASS, SAND] 476 | 477 | # The current block the user can place. Hit num keys to cycle. 478 | self.block = self.inventory[0] 479 | 480 | # Convenience list of num keys. 481 | self.num_keys = [ 482 | key._1, key._2, key._3, key._4, key._5, 483 | key._6, key._7, key._8, key._9, key._0] 484 | 485 | # Instance of the model that handles the world. 486 | self.model = Model() 487 | 488 | # The label that is displayed in the top left of the canvas. 489 | self.label = pyglet.text.Label('', font_name='Arial', font_size=18, 490 | x=10, y=self.height - 10, anchor_x='left', anchor_y='top', 491 | color=(0, 0, 0, 255)) 492 | 493 | # This call schedules the `update()` method to be called 494 | # TICKS_PER_SEC. This is the main game event loop. 495 | pyglet.clock.schedule_interval(self.update, 1.0 / TICKS_PER_SEC) 496 | 497 | def set_exclusive_mouse(self, exclusive): 498 | """ If `exclusive` is True, the game will capture the mouse, if False 499 | the game will ignore the mouse. 500 | 501 | """ 502 | super(Window, self).set_exclusive_mouse(exclusive) 503 | self.exclusive = exclusive 504 | 505 | def get_sight_vector(self): 506 | """ Returns the current line of sight vector indicating the direction 507 | the player is looking. 508 | 509 | """ 510 | x, y = self.rotation 511 | # y ranges from -90 to 90, or -pi/2 to pi/2, so m ranges from 0 to 1 and 512 | # is 1 when looking ahead parallel to the ground and 0 when looking 513 | # straight up or down. 514 | m = math.cos(math.radians(y)) 515 | # dy ranges from -1 to 1 and is -1 when looking straight down and 1 when 516 | # looking straight up. 517 | dy = math.sin(math.radians(y)) 518 | dx = math.cos(math.radians(x - 90)) * m 519 | dz = math.sin(math.radians(x - 90)) * m 520 | return (dx, dy, dz) 521 | 522 | def get_motion_vector(self): 523 | """ Returns the current motion vector indicating the velocity of the 524 | player. 525 | 526 | Returns 527 | ------- 528 | vector : tuple of len 3 529 | Tuple containing the velocity in x, y, and z respectively. 530 | 531 | """ 532 | if any(self.strafe): 533 | x, y = self.rotation 534 | strafe = math.degrees(math.atan2(*self.strafe)) 535 | y_angle = math.radians(y) 536 | x_angle = math.radians(x + strafe) 537 | if self.flying: 538 | m = math.cos(y_angle) 539 | dy = math.sin(y_angle) 540 | if self.strafe[1]: 541 | # Moving left or right. 542 | dy = 0.0 543 | m = 1 544 | if self.strafe[0] > 0: 545 | # Moving backwards. 546 | dy *= -1 547 | # When you are flying up or down, you have less left and right 548 | # motion. 549 | dx = math.cos(x_angle) * m 550 | dz = math.sin(x_angle) * m 551 | else: 552 | dy = 0.0 553 | dx = math.cos(x_angle) 554 | dz = math.sin(x_angle) 555 | else: 556 | dy = 0.0 557 | dx = 0.0 558 | dz = 0.0 559 | return (dx, dy, dz) 560 | 561 | def update(self, dt): 562 | """ This method is scheduled to be called repeatedly by the pyglet 563 | clock. 564 | 565 | Parameters 566 | ---------- 567 | dt : float 568 | The change in time since the last call. 569 | 570 | """ 571 | self.model.process_queue() 572 | sector = sectorize(self.position) 573 | if sector != self.sector: 574 | self.model.change_sectors(self.sector, sector) 575 | if self.sector is None: 576 | self.model.process_entire_queue() 577 | self.sector = sector 578 | m = 8 579 | dt = min(dt, 0.2) 580 | for _ in xrange(m): 581 | self._update(dt / m) 582 | 583 | def _update(self, dt): 584 | """ Private implementation of the `update()` method. This is where most 585 | of the motion logic lives, along with gravity and collision detection. 586 | 587 | Parameters 588 | ---------- 589 | dt : float 590 | The change in time since the last call. 591 | 592 | """ 593 | # walking 594 | speed = FLYING_SPEED if self.flying else WALKING_SPEED 595 | d = dt * speed # distance covered this tick. 596 | dx, dy, dz = self.get_motion_vector() 597 | # New position in space, before accounting for gravity. 598 | dx, dy, dz = dx * d, dy * d, dz * d 599 | # gravity 600 | if not self.flying: 601 | # Update your vertical speed: if you are falling, speed up until you 602 | # hit terminal velocity; if you are jumping, slow down until you 603 | # start falling. 604 | self.dy -= dt * GRAVITY 605 | self.dy = max(self.dy, -TERMINAL_VELOCITY) 606 | dy += self.dy * dt 607 | # collisions 608 | x, y, z = self.position 609 | x, y, z = self.collide((x + dx, y + dy, z + dz), PLAYER_HEIGHT) 610 | self.position = (x, y, z) 611 | 612 | def collide(self, position, height): 613 | """ Checks to see if the player at the given `position` and `height` 614 | is colliding with any blocks in the world. 615 | 616 | Parameters 617 | ---------- 618 | position : tuple of len 3 619 | The (x, y, z) position to check for collisions at. 620 | height : int or float 621 | The height of the player. 622 | 623 | Returns 624 | ------- 625 | position : tuple of len 3 626 | The new position of the player taking into account collisions. 627 | 628 | """ 629 | # How much overlap with a dimension of a surrounding block you need to 630 | # have to count as a collision. If 0, touching terrain at all counts as 631 | # a collision. If .49, you sink into the ground, as if walking through 632 | # tall grass. If >= .5, you'll fall through the ground. 633 | pad = 0.25 634 | p = list(position) 635 | np = normalize(position) 636 | for face in FACES: # check all surrounding blocks 637 | for i in xrange(3): # check each dimension independently 638 | if not face[i]: 639 | continue 640 | # How much overlap you have with this dimension. 641 | d = (p[i] - np[i]) * face[i] 642 | if d < pad: 643 | continue 644 | for dy in xrange(height): # check each height 645 | op = list(np) 646 | op[1] -= dy 647 | op[i] += face[i] 648 | if tuple(op) not in self.model.world: 649 | continue 650 | p[i] -= (d - pad) * face[i] 651 | if face == (0, -1, 0) or face == (0, 1, 0): 652 | # You are colliding with the ground or ceiling, so stop 653 | # falling / rising. 654 | self.dy = 0 655 | break 656 | return tuple(p) 657 | 658 | def on_mouse_press(self, x, y, button, modifiers): 659 | """ Called when a mouse button is pressed. See pyglet docs for button 660 | amd modifier mappings. 661 | 662 | Parameters 663 | ---------- 664 | x, y : int 665 | The coordinates of the mouse click. Always center of the screen if 666 | the mouse is captured. 667 | button : int 668 | Number representing mouse button that was clicked. 1 = left button, 669 | 4 = right button. 670 | modifiers : int 671 | Number representing any modifying keys that were pressed when the 672 | mouse button was clicked. 673 | 674 | """ 675 | if self.exclusive: 676 | vector = self.get_sight_vector() 677 | block, previous = self.model.hit_test(self.position, vector) 678 | if (button == mouse.RIGHT) or \ 679 | ((button == mouse.LEFT) and (modifiers & key.MOD_CTRL)): 680 | # ON OSX, control + left click = right click. 681 | if previous: 682 | self.model.add_block(previous, self.block) 683 | elif button == pyglet.window.mouse.LEFT and block: 684 | texture = self.model.world[block] 685 | if texture != STONE: 686 | self.model.remove_block(block) 687 | else: 688 | self.set_exclusive_mouse(True) 689 | 690 | def on_mouse_motion(self, x, y, dx, dy): 691 | """ Called when the player moves the mouse. 692 | 693 | Parameters 694 | ---------- 695 | x, y : int 696 | The coordinates of the mouse click. Always center of the screen if 697 | the mouse is captured. 698 | dx, dy : float 699 | The movement of the mouse. 700 | 701 | """ 702 | if self.exclusive: 703 | m = 0.15 704 | x, y = self.rotation 705 | x, y = x + dx * m, y + dy * m 706 | y = max(-90, min(90, y)) 707 | self.rotation = (x, y) 708 | 709 | def on_key_press(self, symbol, modifiers): 710 | """ Called when the player presses a key. See pyglet docs for key 711 | mappings. 712 | 713 | Parameters 714 | ---------- 715 | symbol : int 716 | Number representing the key that was pressed. 717 | modifiers : int 718 | Number representing any modifying keys that were pressed. 719 | 720 | """ 721 | if symbol == key.W: 722 | self.strafe[0] -= 1 723 | elif symbol == key.S: 724 | self.strafe[0] += 1 725 | elif symbol == key.A: 726 | self.strafe[1] -= 1 727 | elif symbol == key.D: 728 | self.strafe[1] += 1 729 | elif symbol == key.SPACE: 730 | if self.dy == 0: 731 | self.dy = JUMP_SPEED 732 | elif symbol == key.ESCAPE: 733 | self.set_exclusive_mouse(False) 734 | elif symbol == key.TAB: 735 | self.flying = not self.flying 736 | elif symbol in self.num_keys: 737 | index = (symbol - self.num_keys[0]) % len(self.inventory) 738 | self.block = self.inventory[index] 739 | 740 | def on_key_release(self, symbol, modifiers): 741 | """ Called when the player releases a key. See pyglet docs for key 742 | mappings. 743 | 744 | Parameters 745 | ---------- 746 | symbol : int 747 | Number representing the key that was pressed. 748 | modifiers : int 749 | Number representing any modifying keys that were pressed. 750 | 751 | """ 752 | if symbol == key.W: 753 | self.strafe[0] += 1 754 | elif symbol == key.S: 755 | self.strafe[0] -= 1 756 | elif symbol == key.A: 757 | self.strafe[1] += 1 758 | elif symbol == key.D: 759 | self.strafe[1] -= 1 760 | 761 | def on_resize(self, width, height): 762 | """ Called when the window is resized to a new `width` and `height`. 763 | 764 | """ 765 | # label 766 | self.label.y = height - 10 767 | # reticle 768 | if self.reticle: 769 | self.reticle.delete() 770 | x, y = self.width // 2, self.height // 2 771 | n = 10 772 | self.reticle = pyglet.graphics.vertex_list(4, 773 | ('v2i', (x - n, y, x + n, y, x, y - n, x, y + n)) 774 | ) 775 | 776 | def set_2d(self): 777 | """ Configure OpenGL to draw in 2d. 778 | 779 | """ 780 | width, height = self.get_size() 781 | glDisable(GL_DEPTH_TEST) 782 | viewport = self.get_viewport_size() 783 | glViewport(0, 0, max(1, viewport[0]), max(1, viewport[1])) 784 | glMatrixMode(GL_PROJECTION) 785 | glLoadIdentity() 786 | glOrtho(0, max(1, width), 0, max(1, height), -1, 1) 787 | glMatrixMode(GL_MODELVIEW) 788 | glLoadIdentity() 789 | 790 | def set_3d(self): 791 | """ Configure OpenGL to draw in 3d. 792 | 793 | """ 794 | width, height = self.get_size() 795 | glEnable(GL_DEPTH_TEST) 796 | viewport = self.get_viewport_size() 797 | glViewport(0, 0, max(1, viewport[0]), max(1, viewport[1])) 798 | glMatrixMode(GL_PROJECTION) 799 | glLoadIdentity() 800 | gluPerspective(65.0, width / float(height), 0.1, 60.0) 801 | glMatrixMode(GL_MODELVIEW) 802 | glLoadIdentity() 803 | x, y = self.rotation 804 | glRotatef(x, 0, 1, 0) 805 | glRotatef(-y, math.cos(math.radians(x)), 0, math.sin(math.radians(x))) 806 | x, y, z = self.position 807 | glTranslatef(-x, -y, -z) 808 | 809 | def on_draw(self): 810 | """ Called by pyglet to draw the canvas. 811 | 812 | """ 813 | self.clear() 814 | self.set_3d() 815 | glColor3d(1, 1, 1) 816 | self.model.batch.draw() 817 | self.draw_focused_block() 818 | self.set_2d() 819 | self.draw_label() 820 | self.draw_reticle() 821 | 822 | def draw_focused_block(self): 823 | """ Draw black edges around the block that is currently under the 824 | crosshairs. 825 | 826 | """ 827 | vector = self.get_sight_vector() 828 | block = self.model.hit_test(self.position, vector)[0] 829 | if block: 830 | x, y, z = block 831 | vertex_data = cube_vertices(x, y, z, 0.51) 832 | glColor3d(0, 0, 0) 833 | glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) 834 | pyglet.graphics.draw(24, GL_QUADS, ('v3f/static', vertex_data)) 835 | glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) 836 | 837 | def draw_label(self): 838 | """ Draw the label in the top left of the screen. 839 | 840 | """ 841 | x, y, z = self.position 842 | self.label.text = '%02d (%.2f, %.2f, %.2f) %d / %d' % ( 843 | pyglet.clock.get_fps(), x, y, z, 844 | len(self.model._shown), len(self.model.world)) 845 | self.label.draw() 846 | 847 | def draw_reticle(self): 848 | """ Draw the crosshairs in the center of the screen. 849 | 850 | """ 851 | glColor3d(0, 0, 0) 852 | self.reticle.draw(GL_LINES) 853 | 854 | 855 | def setup_fog(): 856 | """ Configure the OpenGL fog properties. 857 | 858 | """ 859 | # Enable fog. Fog "blends a fog color with each rasterized pixel fragment's 860 | # post-texturing color." 861 | glEnable(GL_FOG) 862 | # Set the fog color. 863 | glFogfv(GL_FOG_COLOR, (GLfloat * 4)(0.5, 0.69, 1.0, 1)) 864 | # Say we have no preference between rendering speed and quality. 865 | glHint(GL_FOG_HINT, GL_DONT_CARE) 866 | # Specify the equation used to compute the blending factor. 867 | glFogi(GL_FOG_MODE, GL_LINEAR) 868 | # How close and far away fog starts and ends. The closer the start and end, 869 | # the denser the fog in the fog range. 870 | glFogf(GL_FOG_START, 20.0) 871 | glFogf(GL_FOG_END, 60.0) 872 | 873 | 874 | def setup(): 875 | """ Basic OpenGL configuration. 876 | 877 | """ 878 | # Set the color of "clear", i.e. the sky, in rgba. 879 | glClearColor(0.5, 0.69, 1.0, 1) 880 | # Enable culling (not rendering) of back-facing facets -- facets that aren't 881 | # visible to you. 882 | glEnable(GL_CULL_FACE) 883 | # Set the texture minification/magnification function to GL_NEAREST (nearest 884 | # in Manhattan distance) to the specified texture coordinates. GL_NEAREST 885 | # "is generally faster than GL_LINEAR, but it can produce textured images 886 | # with sharper edges because the transition between texture elements is not 887 | # as smooth." 888 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) 889 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) 890 | setup_fog() 891 | 892 | 893 | def main(): 894 | window = Window(width=800, height=600, caption='Pyglet', resizable=True) 895 | # Hide the mouse cursor and prevent the mouse from leaving the window. 896 | window.set_exclusive_mouse(True) 897 | setup() 898 | pyglet.app.run() 899 | 900 | 901 | if __name__ == '__main__': 902 | main() 903 | --------------------------------------------------------------------------------