├── .gitignore ├── res └── texture.png ├── nakefile.nim ├── minecraft.html ├── README.md ├── minecraft.nimble ├── .travis.yml ├── deployGHPages.sh ├── LICENSE └── minecraft.nim /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | -------------------------------------------------------------------------------- /res/texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/minecraft/HEAD/res/texture.png -------------------------------------------------------------------------------- /nakefile.nim: -------------------------------------------------------------------------------- 1 | import nimx.naketools 2 | 3 | beforeBuild = proc(b: Builder) = 4 | b.mainFile = "minecraft" 5 | #b.disableClosureCompiler = true 6 | -------------------------------------------------------------------------------- /minecraft.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # minecraft [](https://travis-ci.org/yglukhov/minecraft) 2 | 3 | [Live Demo](http://yglukhov.github.io/minecraft/main.html) 4 | -------------------------------------------------------------------------------- /minecraft.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | version = "0.1" 3 | author = "Yuriy Glukhov" 4 | description = "Game" 5 | license = "BSD" 6 | #bin = "main" 7 | 8 | # Dependencies 9 | requires "https://github.com/yglukhov/rod.git" 10 | requires "nimx" 11 | requires "nake" 12 | requires "closure_compiler" 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | before_install: 5 | - docker pull yglukhov/nim-ui-base 6 | script: 7 | - docker run -v "$(pwd):/project" -w /project yglukhov/nim-ui-base run "nimble install -y && nake --norun && nake js -d:release --norun" && ./deployGHPages.sh ./build/js 8 | -------------------------------------------------------------------------------- /deployGHPages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script requires GH_KEY environment variable to be set to Github access 4 | # token. The token may be generated at https://github.com/settings/tokens and 5 | # has to have permissions to commit (e.g. public_repo) 6 | 7 | set -e 8 | 9 | # The variable itself then has to be set in travis-ci project settings. 10 | if [ "$GH_KEY" \!= "" ] 11 | then 12 | export "GIT_DIR=$HOME/deployGHPages.git" 13 | export "GIT_WORK_TREE=$1" 14 | mkdir -p "$GIT_DIR" 15 | 16 | git init 17 | 18 | git config user.name "Travis CI" 19 | git config user.email "autodocgen@example.com" 20 | 21 | git add . 22 | 23 | git commit -m "Deploy to GitHub Pages" 24 | 25 | git push --force --quiet "https://$GH_KEY@github.com/$TRAVIS_REPO_SLUG" master:gh-pages > /dev/null 2>&1 26 | fi 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yuriy Glukhov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /minecraft.nim: -------------------------------------------------------------------------------- 1 | import math, times, tables, lists, sets, random 2 | 3 | import opengl 4 | 5 | import nimx/[window, matrixes, image, animation, system_logger, portable_gl, context, keyboard, view_event_handling_new] 6 | import nimx/assets/asset_manager 7 | import rod/[viewport, node, component, quaternion] 8 | import rod/component/camera 9 | 10 | const enableEditor = not defined(release) 11 | 12 | when enableEditor: 13 | import rod.edit_view 14 | 15 | type iVec3 = TVector[3, GLint] 16 | 17 | 18 | const TICKS_PER_SEC = 60 19 | 20 | # Size of sectors used to ease block loading. 21 | const SECTOR_SIZE = 16 22 | 23 | const WALKING_SPEED = 5 24 | const FLYING_SPEED = 15 25 | 26 | const GRAVITY = 20.0 27 | const MAX_JUMP_HEIGHT = 1.0 # About the height of a block. 28 | # To derive the formula for calculating jump speed, first solve 29 | # v_t = v_0 + a * t 30 | # for the time at which you achieve maximum height, where a is the acceleration 31 | # due to gravity and v_t = 0. This gives: 32 | # t = - v_0 / a 33 | # Use t and the desired MAX_JUMP_HEIGHT to solve for v_0 (jump speed) in 34 | # s = s_0 + v_0 * t + (a * t^2) / 2 35 | const JUMP_SPEED = sqrt(2 * GRAVITY * MAX_JUMP_HEIGHT) 36 | const TERMINAL_VELOCITY = 50 37 | 38 | const PLAYER_HEIGHT = 2 39 | 40 | var vertexBuffer : BufferRef 41 | var indexBuffer: BufferRef 42 | var uvBuffer: BufferRef 43 | 44 | 45 | proc cube_vertices(x, y, z, n: float32): seq[float32] = 46 | ## Return the vertices of the cube at position x, y, z with size 2*n. 47 | @[ 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, # top 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, # bottom 50 | 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 51 | 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 52 | 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 53 | 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 54 | ] 55 | 56 | 57 | 58 | proc tex_coord(x, y: int, n: float32 = 4): seq[float32] = 59 | ## Return the bounding vertices of the texture square. 60 | let m = 1'f32 / n 61 | let dx = x.float32 * m 62 | let dy = y.float32 * m 63 | return @[dx, dy, dx + m, dy, dx + m, dy + m, dx, dy + m] 64 | 65 | type BlockTexture = ref object 66 | uv: seq[float32] 67 | offsetInUVBuffer: int 68 | 69 | proc tex_coords(top, bottom, side: tuple[x, y: int]): BlockTexture = 70 | ## Return a list of the texture squares for the top, bottom and side. 71 | 72 | result.new() 73 | 74 | result.uv = tex_coord(top.x, top.y) 75 | result.uv.add(tex_coord(bottom.x, bottom.y)) 76 | let side = tex_coord(side.x, side.y) 77 | result.uv.add(side) 78 | result.uv.add(side) 79 | result.uv.add(side) 80 | result.uv.add(side) 81 | 82 | let GRASS = tex_coords((1, 0), (0, 1), (0, 0)) 83 | let SAND = tex_coords((1, 1), (1, 1), (1, 1)) 84 | let BRICK = tex_coords((2, 0), (2, 0), (2, 0)) 85 | let STONE = tex_coords((2, 1), (2, 1), (2, 1)) 86 | 87 | 88 | proc createBuffers() = 89 | let gl = currentContext().gl 90 | vertexBuffer = gl.createBuffer() 91 | indexBuffer = gl.createBuffer() 92 | uvBuffer = gl.createBuffer() 93 | 94 | gl.enable(gl.CULL_FACE) 95 | 96 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer) 97 | gl.bufferData(gl.ARRAY_BUFFER, cube_vertices(0, 0, 0, 0.5), gl.STATIC_DRAW) 98 | 99 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer) 100 | let indexData = [0.GLubyte, 1, 2, 0, 2, 3, # top 101 | 4, 5, 6, 4, 6, 7, # bottom 102 | 8, 9, 10, 8, 10, 11, # left 103 | 12, 13, 14, 12, 14, 15, # right 104 | 16, 17, 18, 16, 18, 19, # front 105 | 20, 21, 22, 20, 22, 23 # back 106 | ] 107 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW) 108 | 109 | var uvData = GRASS.uv 110 | SAND.offsetInUVBuffer = uvData.len * sizeof(GLfloat) 111 | uvData.add(SAND.uv) 112 | BRICK.offsetInUVBuffer = uvData.len * sizeof(GLfloat) 113 | uvData.add(BRICK.uv) 114 | STONE.offsetInUVBuffer = uvData.len * sizeof(GLfloat) 115 | uvData.add(STONE.uv) 116 | 117 | gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer) 118 | gl.bufferData(gl.ARRAY_BUFFER, uvData, gl.STATIC_DRAW) 119 | 120 | 121 | let FACES = [ 122 | [ 0.GLint, 1, 0], 123 | [ 0.GLint,-1, 0], 124 | [-1.GLint, 0, 0], 125 | [ 1.GLint, 0, 0], 126 | [ 0.GLint, 0, 1], 127 | [ 0.GLint, 0,-1], 128 | ] 129 | 130 | 131 | proc normalized(position: Vector3): iVec3 = 132 | ##[ Accepts `position` of arbitrary precision and returns the block 133 | containing that position. 134 | 135 | Parameters 136 | ---------- 137 | position : tuple of len 3 138 | 139 | Returns 140 | ------- 141 | block_position : tuple of ints of len 3 142 | 143 | ]## 144 | return [round(position.x).GLint, round(position.y).GLint, round(position.z).GLint] 145 | 146 | 147 | proc sectorize(position: iVec3): iVec3 = 148 | ##[ Returns a tuple representing the sector for the given `position`. 149 | 150 | Parameters 151 | ---------- 152 | position : tuple of len 3 153 | 154 | Returns 155 | ------- 156 | sector : tuple of len 3 157 | 158 | ]## 159 | result.x = GLint(position.x / SECTOR_SIZE) 160 | result.z = GLint(position.z / SECTOR_SIZE) 161 | 162 | proc sectorize(position: Vector3): iVec3 = sectorize(normalized(position)) 163 | 164 | 165 | type Model = ref object 166 | # A Batch is a collection of vertex lists for batched rendering. 167 | batch: int 168 | 169 | # A TextureGroup manages an OpenGL texture. 170 | group: Image 171 | 172 | # A mapping from position to the texture of the block at that position. 173 | # This defines all the blocks that are currently in the world. 174 | world: Table[iVec3, BlockTexture] 175 | 176 | # Same mapping as `world` but only contains blocks that are shown. 177 | shown: Table[iVec3, BlockTexture] 178 | 179 | # Mapping from position to a pyglet `VertextList` for all shown blocks. 180 | shown_vertex_lists: Table[iVec3, int] 181 | 182 | # Mapping from sector to a list of positions inside that sector. 183 | sectors: Table[iVec3, TableRef[iVec3, bool]] 184 | 185 | # Simple function queue implementation. The queue is populated with 186 | # show_block_aux() and hide_block_aux() calls 187 | queue: SinglyLinkedList[proc()] 188 | 189 | proc initialize(self: Model) 190 | 191 | proc newModel(): Model = 192 | result.new() 193 | 194 | let r = result 195 | sharedAssetManager().getAssetAtPath("texture.png") do(i: Image, err: string): 196 | r.group = i 197 | 198 | # A mapping from position to the texture of the block at that position. 199 | # This defines all the blocks that are currently in the world. 200 | result.world = initTable[iVec3, BlockTexture]() 201 | 202 | # Same mapping as `world` but only contains blocks that are shown. 203 | result.shown = initTable[iVec3, BlockTexture]() 204 | 205 | # Mapping from sector to a list of positions inside that sector. 206 | result.sectors = initTable[iVec3, TableRef[iVec3, bool]]() 207 | 208 | result.initialize() 209 | 210 | proc randint(a, b: int): int = random(b - a + 1) + a 211 | 212 | proc add_block(self: Model, position: iVec3, texture: BlockTexture, immediate=true) 213 | 214 | proc initialize(self: Model) = 215 | ## Initialize the world by placing all the blocks. 216 | 217 | let n = 80.GLint # 1/2 width and height of world 218 | let y = 0.GLint # initial y height 219 | for x in -n .. n: 220 | for z in -n .. n: 221 | # create a layer stone an grass everywhere. 222 | self.add_block([x, y - 2, z], GRASS, immediate=false) 223 | self.add_block([x, y - 3, z], STONE, immediate=false) 224 | if abs(x) == n or abs(z) == n: 225 | # create outer walls. 226 | for dy in -2 .. 2: 227 | self.add_block([x, y + dy.GLint, z], STONE, immediate=false) 228 | 229 | # generate the hills randomly 230 | let o = n - 10 231 | for _ in 0 ..< 120: 232 | let a = randint(-o, o).GLint # x position of the hill 233 | let b = randint(-o, o).GLint # z position of the hill 234 | let c = -1.GLint # base of the hill 235 | let h = randint(1, 6).GLint # height of the hill 236 | var s = randint(4, 8).GLint # 2 * s is the side length of the hill 237 | const d = 1 # how quickly to taper off the hills 238 | let t = [GRASS, SAND, BRICK][random(3)] 239 | for y in c ..< c + h: 240 | for x in a - s .. a + s: 241 | for z in b - s .. b + s: 242 | if (x - a) ^ 2 + (z - b) ^ 2 > (s + 1) ^ 2: 243 | continue 244 | if (x - 0) ^ 2 + (z - 0) ^ 2 < 5 ^ 2: 245 | continue 246 | self.add_block([x, y, z], t, immediate=false) 247 | s -= d # decrement side lenth so hills taper off 248 | 249 | proc hit_test(self: Model, position: Vector3, vector: Vector3, max_distance: int = 8): tuple[ok: bool, bl, prev: iVec3] = 250 | ##[ Line of sight search from current position. If a block is 251 | intersected it is returned, along with the block previously in the line 252 | of sight. If no block is found, return None, None. 253 | 254 | Parameters 255 | ---------- 256 | position : tuple of len 3 257 | The (x, y, z) position to check visibility from. 258 | vector : tuple of len 3 259 | The line of sight vector. 260 | max_distance : int 261 | How many blocks away to search for a hit. 262 | 263 | ]## 264 | let m = 8 265 | var p = position 266 | let dp = vector / float32(m) 267 | var previous : iVec3 268 | for i in 0 ..< max_distance * m: 269 | var key = p.normalized() 270 | if key != previous and key in self.world: 271 | result.ok = true 272 | result.bl = key 273 | result.prev = previous 274 | return 275 | previous = key 276 | p += dp 277 | 278 | proc exposed(self: Model, position: iVec3): bool = 279 | ##[ Returns False is given `position` is surrounded on all 6 sides by 280 | blocks, True otherwise. 281 | 282 | ]## 283 | for f in FACES: 284 | if (f + position) notin self.world: 285 | return true 286 | return false 287 | 288 | proc hide_block(self: Model, position: iVec3, immediate=true) 289 | proc check_neighbors(self: Model, position: iVec3) 290 | 291 | template excl(t: var Table[iVec3, bool] | TableRef[iVec3, bool], k: iVec3) = t.del(k) 292 | template incl(t: var Table[iVec3, bool] | TableRef[iVec3, bool], k: iVec3) = t[k] = true 293 | 294 | proc remove_block(self: Model, position: iVec3, immediate=true) = 295 | ##[ Remove the block at the given `position`. 296 | 297 | Parameters 298 | ---------- 299 | position : tuple of len 3 300 | The (x, y, z) position of the block to remove. 301 | immediate : bool 302 | Whether or not to immediately remove block from canvas. 303 | 304 | ]## 305 | self.world.del(position) 306 | self.sectors[sectorize(position)].excl(position) 307 | if immediate or true: 308 | if position in self.shown: 309 | self.hide_block(position) 310 | self.check_neighbors(position) 311 | 312 | template setdefault[K, V](t: var Table[K, V], k: K, d: V): var V = 313 | let kk = k 314 | if kk notin t: t[kk] = d 315 | t[kk] 316 | 317 | proc show_block(self: Model, position: iVec3, immediate=true) 318 | 319 | proc add_block(self: Model, position: iVec3, texture: BlockTexture, immediate=true) = 320 | ##[ Add a block with the given `texture` and `position` to the world. 321 | 322 | Parameters 323 | ---------- 324 | position : tuple of len 3 325 | The (x, y, z) position of the block to add. 326 | texture : list of len 3 327 | The coordinates of the texture squares. Use `tex_coords()` to 328 | generate. 329 | immediate : bool 330 | Whether or not to draw the block immediately. 331 | 332 | ]## 333 | if position in self.world: 334 | self.remove_block(position, immediate) 335 | self.world[position] = texture 336 | let sp = sectorize(position) 337 | if sp in self.sectors: 338 | self.sectors[sp].incl(position) 339 | else: 340 | self.sectors[sp] = newTable[iVec3, bool]() 341 | self.sectors[sp].incl(position) 342 | 343 | if immediate or true: 344 | if self.exposed(position): 345 | self.show_block(position) 346 | self.check_neighbors(position) 347 | 348 | proc check_neighbors(self: Model, position: iVec3) = 349 | ##[ Check all blocks surrounding `position` and ensure their visual 350 | state is current. This means hiding blocks that are not exposed and 351 | ensuring that all exposed blocks are shown. Usually used after a block 352 | is added or removed. 353 | 354 | ]## 355 | for f in FACES: 356 | let key = position + f 357 | if key notin self.world: 358 | continue 359 | if self.exposed(key): 360 | if key notin self.shown: 361 | self.show_block(key) 362 | else: 363 | if key in self.shown: 364 | self.hide_block(key) 365 | 366 | proc show_block_aux(self: Model, position: iVec3, texture: BlockTexture) 367 | proc enqueue(self: Model, f: proc()) 368 | 369 | proc show_block(self: Model, position: iVec3, immediate=true) = 370 | ##[ Show the block at the given `position`. This method assumes the 371 | block has already been added with add_block() 372 | 373 | Parameters 374 | ---------- 375 | position : tuple of len 3 376 | The (x, y, z) position of the block to show. 377 | immediate : bool 378 | Whether or not to show the block immediately. 379 | 380 | ]## 381 | let texture = self.world[position] 382 | self.shown[position] = texture 383 | if immediate: 384 | self.show_block_aux(position, texture) 385 | else: 386 | self.enqueue(proc() = self.show_block_aux(position, texture)) 387 | 388 | proc show_block_aux(self: Model, position: iVec3, texture: BlockTexture) = 389 | ##[ Private implementation of the `show_block()` method. 390 | 391 | Parameters 392 | ---------- 393 | position : tuple of len 3 394 | The (x, y, z) position of the block to show. 395 | texture : list of len 3 396 | The coordinates of the texture squares. Use `tex_coords()` to 397 | generate. 398 | 399 | ]## 400 | discard 401 | #let vertex_data = cube_vertices(position.x.float32, position.y.float32, position.z.float32, 0.5) 402 | # create vertex list 403 | # FIXME Maybe `add_indexed()` should be used instead 404 | #self.shown_vertex_lists[position] = self.batch.add(24, GL_QUADS, self.group, 405 | # ("v3f/static", vertex_data), 406 | # ("t2f/static", texture.uv)) 407 | 408 | proc hide_block_aux(self: Model, position: iVec3) = 409 | ##[ Private implementation of the 'hide_block()` method. 410 | 411 | ]## 412 | #self.shown_vertex_lists.pop(position).delete() 413 | discard 414 | 415 | proc hide_block(self: Model, position: iVec3, immediate=true) = 416 | ##[ Hide the block at the given `position`. Hiding does not remove the 417 | block from the world. 418 | 419 | Parameters 420 | ---------- 421 | position : tuple of len 3 422 | The (x, y, z) position of the block to hide. 423 | immediate : bool 424 | Whether or not to immediately remove the block from the canvas. 425 | 426 | ]## 427 | self.shown.del(position) 428 | if immediate or true: 429 | self.hide_block_aux(position) 430 | else: 431 | self.enqueue(proc() = self.hide_block_aux(position)) 432 | 433 | proc show_sector(self: Model, sector: iVec3) = 434 | ##[ Ensure all blocks in the given sector that should be shown are 435 | drawn to the canvas. 436 | 437 | ]## 438 | let s = self.sectors.getOrDefault(sector) 439 | if not s.isNil: 440 | for position, _ in s: 441 | if position notin self.shown and self.exposed(position): 442 | self.show_block(position, false) 443 | 444 | proc hide_sector(self: Model, sector: iVec3) = 445 | ##[ Ensure all blocks in the given sector that should be hidden are 446 | removed from the canvas. 447 | 448 | ]## 449 | let s = self.sectors.getOrDefault(sector) 450 | if not s.isNil: 451 | for position, _ in s: 452 | if position in self.shown: 453 | self.hide_block(position, false) 454 | 455 | proc change_sectors(self: Model, before, after: iVec3) = 456 | ##[ Move from sector `before` to sector `after`. A sector is a 457 | contiguous x, y sub-region of world. Sectors are used to speed up 458 | world rendering. 459 | 460 | ]## 461 | var before_set = initTable[iVec3, bool]() 462 | var after_set = initTable[iVec3, bool]() 463 | let pad = 4.GLint 464 | for dx in -pad .. pad: 465 | for dy in [0.GLint]: # xrange(-pad, pad + 1): 466 | for dz in -pad .. pad: 467 | if dx ^ 2 + dy ^ 2 + dz ^ 2 > (pad + 1) ^ 2: 468 | continue 469 | before_set.incl([before.x + dx, before.y + dy, before.z + dz]) 470 | after_set.incl([after.x + dx, after.y + dy, after.z + dz]) 471 | for k, _ in before_set: 472 | if k notin after_set: 473 | self.hide_sector(k) 474 | 475 | for k, _ in after_set: 476 | if k notin before_set: 477 | self.show_sector(k) 478 | 479 | template isEmpty(L: SinglyLinkedList): bool = L.head.isNil 480 | 481 | proc popFront[T](L: var SinglyLinkedList[T]): SinglyLinkedNode[T] = 482 | result = L.head 483 | L.head = L.head.next 484 | 485 | proc append[T](L: var SinglyLinkedList[T]; n: SinglyLinkedNode[T]) = 486 | if not L.tail.isNil: 487 | L.tail.next = n 488 | L.tail = n 489 | 490 | proc enqueue(self: Model, f: proc()) = 491 | ## Add `f` to the internal queue. 492 | self.queue.append(newSinglyLinkedNode(f)) 493 | 494 | proc dequeue(self: Model) = 495 | ## Pop the top function from the internal queue and call it. 496 | let f = self.queue.popFront().value 497 | f() 498 | 499 | proc process_queue(self: Model) = 500 | #[ Process the entire queue while taking periodic breaks. This allows 501 | the game loop to run smoothly. The queue contains calls to 502 | show_block_aux() and hide_block_aux() so this method should be called if 503 | add_block() or remove_block() was called with immediate=False 504 | 505 | ]# 506 | let start = epochTime() 507 | while not self.queue.isEmpty and epochTime() - start < 1.0 / TICKS_PER_SEC: 508 | self.dequeue() 509 | 510 | proc process_entire_queue(self: Model) = 511 | ## Process the entire queue with no breaks. 512 | while not self.queue.isEmpty: 513 | self.dequeue() 514 | 515 | type GameView = ref object of SceneView 516 | exclusive: bool # Whether or not the window exclusively captures the mouse. 517 | flying: bool # When flying gravity has no effect and speed is increased. 518 | 519 | # Strafing is moving lateral to the direction you are facing, 520 | # e.g. moving to the left or right while continuing to face forward. 521 | # 522 | # First element is -1 when moving forward, 1 when moving back, and 0 523 | # otherwise. The second element is -1 when moving left, 1 when moving 524 | # right, and 0 otherwise. 525 | strafe: TVector[2, int] 526 | 527 | # Current (x, y, z) position in the world, specified with floats. Note 528 | # that, perhaps unlike in math class, the y-axis is the vertical axis. 529 | position: Vector3 530 | 531 | # First element is rotation of the player in the x-z plane (ground 532 | # plane) measured from the z-axis down. The second is the rotation 533 | # angle from the ground plane up. Rotation is in degrees. 534 | # 535 | # The vertical plane rotation ranges from -90 (looking straight down) to 536 | # 90 (looking straight up). The horizontal rotation range is unbounded. 537 | rotation: Vector2 538 | 539 | # Which sector the player is currently in. 540 | sector: iVec3 # = None 541 | 542 | dy: float32 # Velocity in the y (upward) direction. 543 | 544 | inventory: seq[BlockTexture] # A list of blocks the player can place. Hit num keys to cycle. 545 | 546 | blockInHand: BlockTexture # The current block the user can place. Hit num keys to cycle. 547 | 548 | model: Model 549 | 550 | proc update(self: GameView, dt: float32) 551 | 552 | method init(self: GameView, r: Rect) = 553 | procCall self.SceneView.init(r) 554 | 555 | # The crosshairs at the center of the screen. 556 | #self.reticle = nil 557 | 558 | # A list of blocks the player can place. Hit num keys to cycle. 559 | self.inventory = @[BRICK, GRASS, SAND] 560 | 561 | # The current block the user can place. Hit num keys to cycle. 562 | self.blockInHand = self.inventory[0] 563 | 564 | # Convenience list of num keys. 565 | #self.num_keys = [ 566 | # key._1, key._2, key._3, key._4, key._5, 567 | # key._6, key._7, key._8, key._9, key._0] 568 | 569 | # Instance of the model that handles the world. 570 | self.model = newModel() 571 | 572 | # The label that is displayed in the top left of the canvas. 573 | #self.label = pyglet.text.Label('', font_name='Arial', font_size=18, 574 | # x=10, y=self.height - 10, anchor_x='left', anchor_y='top', 575 | # color=(0, 0, 0, 255)) 576 | 577 | # This call schedules the `update()` method to be called 578 | # TICKS_PER_SEC. This is the main game event loop. 579 | 580 | 581 | # pyglet.clock.schedule_interval(self.update, 1.0 / TICKS_PER_SEC) 582 | 583 | #[ 584 | def set_exclusive_mouse(self, exclusive): 585 | """ If `exclusive` is True, the game will capture the mouse, if False 586 | the game will ignore the mouse. 587 | 588 | """ 589 | super(Window, self).set_exclusive_mouse(exclusive) 590 | self.exclusive = exclusive 591 | ]# 592 | proc get_sight_vector(self: GameView): Vector3 = 593 | ##[ Returns the current line of sight vector indicating the direction 594 | the player is looking. 595 | 596 | ]## 597 | let x = self.rotation.x 598 | let y = self.rotation.y 599 | # y ranges from -90 to 90, or -pi/2 to pi/2, so m ranges from 0 to 1 and 600 | # is 1 when looking ahead parallel to the ground and 0 when looking 601 | # straight up or down. 602 | let m = cos(degToRad(y)) 603 | # dy ranges from -1 to 1 and is -1 when looking straight down and 1 when 604 | # looking straight up. 605 | result.y = sin(degToRad(y)) 606 | result.x = cos(degToRad(x - 90)) * m 607 | result.z = sin(degToRad(x - 90)) * m 608 | 609 | proc get_motion_vector(self: GameView): Vector3 = 610 | ##[ Returns the current motion vector indicating the velocity of the 611 | player. 612 | 613 | Returns 614 | ------- 615 | vector : tuple of len 3 616 | Tuple containing the velocity in x, y, and z respectively. 617 | 618 | ]## 619 | if self.strafe.x != 0 or self.strafe.y != 0: 620 | let strafe = radToDeg(arctan2(self.strafe.x.float, self.strafe.y.float)) 621 | let y_angle = degToRad(self.rotation.y) 622 | let x_angle = degToRad(self.rotation.x + strafe) 623 | if self.flying: 624 | var m = cos(y_angle) 625 | result.y = sin(y_angle) 626 | if self.strafe.y != 0: 627 | # Moving left or right. 628 | result.y = 0.0 629 | m = 1 630 | if self.strafe.x > 0: 631 | # Moving backwards. 632 | result.y *= -1 633 | # When you are flying up or down, you have less left and right 634 | # motion. 635 | result.x = sin(x_angle) * m 636 | result.z = cos(x_angle) * m 637 | else: 638 | result.x = sin(x_angle) 639 | result.z = cos(x_angle) 640 | 641 | proc update_aux(self: GameView, dt: float32) 642 | 643 | proc update(self: GameView, dt: float32) = 644 | ##[ This method is scheduled to be called repeatedly by the pyglet 645 | clock. 646 | 647 | Parameters 648 | ---------- 649 | dt : float 650 | The change in time since the last call. 651 | 652 | ]## 653 | self.model.process_queue() 654 | let sector = sectorize(self.position) 655 | if sector != self.sector: 656 | self.model.change_sectors(self.sector, sector) 657 | #if self.sector is None: 658 | #block: 659 | # self.model.process_entire_queue() 660 | self.sector = sector 661 | let m = 8 662 | let mdt = min(dt, 0.2) 663 | for _ in 0 ..< m: 664 | self.update_aux(mdt / m.float) 665 | 666 | proc collide(self: GameView, position: Vector3, height: GLint): Vector3 667 | 668 | proc update_aux(self: GameView, dt: float32) = 669 | ##[ Private implementation of the `update()` method. This is where most 670 | of the motion logic lives, along with gravity and collision detection. 671 | 672 | Parameters 673 | ---------- 674 | dt : float 675 | The change in time since the last call. 676 | 677 | ]## 678 | # walking 679 | let speed = if self.flying: FLYING_SPEED else: WALKING_SPEED 680 | let d = dt * speed.float32 # distance covered this tick. 681 | var mv = self.get_motion_vector() 682 | # New position in space, before accounting for gravity. 683 | mv *= d 684 | # gravity 685 | if not self.flying: 686 | # Update your vertical speed: if you are falling, speed up until you 687 | # hit terminal velocity; if you are jumping, slow down until you 688 | # start falling. 689 | self.dy -= dt * GRAVITY 690 | self.dy = max(self.dy, -TERMINAL_VELOCITY) 691 | mv.y += self.dy * dt 692 | # collisions 693 | self.position = self.collide(self.position + mv, PLAYER_HEIGHT) 694 | self.camera.node.position = self.position 695 | 696 | proc collide(self: GameView, position: Vector3, height: GLint): Vector3 = 697 | ##[ Checks to see if the player at the given `position` and `height` 698 | is colliding with any blocks in the world. 699 | 700 | Parameters 701 | ---------- 702 | position : tuple of len 3 703 | The (x, y, z) position to check for collisions at. 704 | height : int or float 705 | The height of the player. 706 | 707 | Returns 708 | ------- 709 | position : tuple of len 3 710 | The new position of the player taking into account collisions. 711 | 712 | ]## 713 | # How much overlap with a dimension of a surrounding block you need to 714 | # have to count as a collision. If 0, touching terrain at all counts as 715 | # a collision. If .49, you sink into the ground, as if walking through 716 | # tall grass. If >= .5, you'll fall through the ground. 717 | let pad = 0.25 718 | result = position 719 | let np = normalized(position) 720 | for face in FACES: # check all surrounding blocks 721 | for i in 0 .. 2: # check each dimension independently 722 | if face[i] == 0: 723 | continue 724 | # How much overlap you have with this dimension. 725 | let d = (result[i] - np[i].Coord) * face[i].Coord 726 | if d < pad: 727 | continue 728 | for dy in 0 ..< height: # check each height 729 | var op = np 730 | op.y -= dy 731 | op[i] += face[i] 732 | if op notin self.model.world: 733 | continue 734 | result[i] -= (d - pad) * face[i].float 735 | if face == [0.GLint, -1, 0] or face == [0.GLint, 1, 0]: 736 | # You are colliding with the ground or ceiling, so stop 737 | # falling / rising. 738 | self.dy = 0 739 | break 740 | 741 | #[ 742 | proc on_mouse_press(self: GameView, x, y: float32, button, modifiers): 743 | """ Called when a mouse button is pressed. See pyglet docs for button 744 | amd modifier mappings. 745 | 746 | Parameters 747 | ---------- 748 | x, y : int 749 | The coordinates of the mouse click. Always center of the screen if 750 | the mouse is captured. 751 | button : int 752 | Number representing mouse button that was clicked. 1 = left button, 753 | 4 = right button. 754 | modifiers : int 755 | Number representing any modifying keys that were pressed when the 756 | mouse button was clicked. 757 | 758 | """ 759 | if self.exclusive: 760 | vector = self.get_sight_vector() 761 | block, previous = self.model.hit_test(self.position, vector) 762 | if (button == mouse.RIGHT) or \ 763 | ((button == mouse.LEFT) and (modifiers & key.MOD_CTRL)): 764 | # ON OSX, control + left click = right click. 765 | if previous: 766 | self.model.add_block(previous, self.block) 767 | elif button == pyglet.window.mouse.LEFT and block: 768 | texture = self.model.world[block] 769 | if texture != STONE: 770 | self.model.remove_block(block) 771 | else: 772 | self.set_exclusive_mouse(True) 773 | ]# 774 | 775 | var lastPos: Point 776 | 777 | method onTouchEv(self: GameView, e: var Event): bool = 778 | ##[ Called when the player moves the mouse. 779 | 780 | Parameters 781 | ---------- 782 | x, y : int 783 | The coordinates of the mouse click. Always center of the screen if 784 | the mouse is captured. 785 | dx, dy : float 786 | The movement of the mouse. 787 | 788 | ]## 789 | discard procCall self.SceneView.onTouchEv(e) 790 | result = true 791 | if self.exclusive or true: 792 | if lastPos != zeroPoint: 793 | let m = 0.15 794 | var x = self.rotation[0] 795 | var y = self.rotation[1] 796 | let d = e.localPosition - lastPos 797 | x += d.x * m 798 | y += d.y * m 799 | y = max(-90, min(90, y)) 800 | self.rotation = [x, y] 801 | self.camera.node.rotation = aroundX(y) * aroundY(x) 802 | 803 | lastPos = e.localPosition 804 | if e.buttonState == bsUp: 805 | lastPos = zeroPoint 806 | 807 | method acceptsFirstResponder(self: GameView): bool = true 808 | 809 | method onKeyDown(self: GameView, e: var Event): bool = 810 | ##[ Called when the player presses a key. See pyglet docs for key 811 | mappings. 812 | 813 | Parameters 814 | ---------- 815 | symbol : int 816 | Number representing the key that was pressed. 817 | modifiers : int 818 | Number representing any modifying keys that were pressed. 819 | 820 | ]## 821 | if e.repeat: return 822 | 823 | case e.keyCode 824 | of VirtualKey.W: 825 | self.strafe[1] -= 1 826 | of VirtualKey.S: 827 | self.strafe[1] += 1 828 | of VirtualKey.A: 829 | self.strafe[0] -= 1 830 | of VirtualKey.D: 831 | self.strafe[0] += 1 832 | of VirtualKey.Space: 833 | if self.dy == 0: 834 | self.dy = JUMP_SPEED 835 | of VirtualKey.Escape: 836 | discard # self.set_exclusive_mouse(False) 837 | of VirtualKey.Tab: 838 | self.flying = not self.flying 839 | else: 840 | discard 841 | #[elif symbol in self.num_keys: 842 | index = (symbol - self.num_keys[0]) % len(self.inventory) 843 | self.block = self.inventory[index] 844 | ]# 845 | 846 | method onKeyUp(self: GameView, e: var Event): bool = 847 | ##[ Called when the player releases a key. See pyglet docs for key 848 | mappings. 849 | 850 | Parameters 851 | ---------- 852 | symbol : int 853 | Number representing the key that was pressed. 854 | modifiers : int 855 | Number representing any modifying keys that were pressed. 856 | 857 | ]## 858 | if e.repeat: return 859 | case e.keyCode 860 | of VirtualKey.W: 861 | self.strafe[1] += 1 862 | of VirtualKey.S: 863 | self.strafe[1] -= 1 864 | of VirtualKey.A: 865 | self.strafe[0] += 1 866 | of VirtualKey.D: 867 | self.strafe[0] -= 1 868 | else: 869 | discard 870 | 871 | #[ 872 | def on_resize(self, width, height): 873 | """ Called when the window is resized to a new `width` and `height`. 874 | 875 | """ 876 | # label 877 | self.label.y = height - 10 878 | # reticle 879 | if self.reticle: 880 | self.reticle.delete() 881 | x, y = self.width / 2, self.height / 2 882 | n = 10 883 | self.reticle = pyglet.graphics.vertex_list(4, 884 | ('v2i', (x - n, y, x + n, y, x, y - n, x, y + n)) 885 | ) 886 | 887 | def set_2d(self): 888 | """ Configure OpenGL to draw in 2d. 889 | 890 | """ 891 | width, height = self.get_size() 892 | glDisable(GL_DEPTH_TEST) 893 | glViewport(0, 0, width, height) 894 | glMatrixMode(GL_PROJECTION) 895 | glLoadIdentity() 896 | glOrtho(0, width, 0, height, -1, 1) 897 | glMatrixMode(GL_MODELVIEW) 898 | glLoadIdentity() 899 | 900 | def set_3d(self): 901 | """ Configure OpenGL to draw in 3d. 902 | 903 | """ 904 | width, height = self.get_size() 905 | glEnable(GL_DEPTH_TEST) 906 | glViewport(0, 0, width, height) 907 | glMatrixMode(GL_PROJECTION) 908 | glLoadIdentity() 909 | gluPerspective(65.0, width / float(height), 0.1, 60.0) 910 | glMatrixMode(GL_MODELVIEW) 911 | glLoadIdentity() 912 | x, y = self.rotation 913 | glRotatef(x, 0, 1, 0) 914 | glRotatef(-y, math.cos(math.radians(x)), 0, math.sin(math.radians(x))) 915 | x, y, z = self.position 916 | glTranslatef(-x, -y, -z) 917 | ]# 918 | method draw(self: GameView, r: Rect) = 919 | procCall self.SceneView.draw(r) 920 | ## Called by pyglet to draw the canvas. 921 | # self.clear() 922 | # self.set_3d() 923 | # glColor3d(1, 1, 1) 924 | #self.model.batch.draw() 925 | # self.draw_focused_block() 926 | # self.set_2d() 927 | # self.draw_reticle() 928 | #[ 929 | def draw_focused_block(self): 930 | """ Draw black edges around the block that is currently under the 931 | crosshairs. 932 | 933 | """ 934 | vector = self.get_sight_vector() 935 | block = self.model.hit_test(self.position, vector)[0] 936 | if block: 937 | x, y, z = block 938 | vertex_data = cube_vertices(x, y, z, 0.51) 939 | glColor3d(0, 0, 0) 940 | glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) 941 | pyglet.graphics.draw(24, GL_QUADS, ('v3f/static', vertex_data)) 942 | glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) 943 | 944 | def draw_reticle(self): 945 | """ Draw the crosshairs in the center of the screen. 946 | 947 | """ 948 | glColor3d(0, 0, 0) 949 | self.reticle.draw(GL_LINES) 950 | ]# 951 | 952 | 953 | var shader : ProgramRef 954 | 955 | let vs = """ 956 | attribute vec4 aPosition; 957 | attribute vec2 aTexCoord; 958 | 959 | uniform mat4 uModelViewProjectionMatrix; 960 | uniform ivec3 uPosOffset; 961 | 962 | varying vec2 vTexCoord; 963 | 964 | void main() { 965 | vTexCoord = aTexCoord; 966 | vec4 p = aPosition; 967 | p.xyz += vec3(uPosOffset); 968 | gl_Position = uModelViewProjectionMatrix * p; 969 | } 970 | """ 971 | 972 | let fs = """ 973 | #ifdef GL_ES 974 | #extension GL_OES_standard_derivatives : enable 975 | precision mediump float; 976 | #endif 977 | 978 | uniform sampler2D texUnit; 979 | varying vec2 vTexCoord; 980 | 981 | void main() { 982 | vec2 uv = vTexCoord; 983 | uv.y = 1.0 - uv.y; 984 | gl_FragColor = texture2D(texUnit, uv); 985 | } 986 | """ 987 | 988 | proc drawModel(m: Model) = 989 | if m.group.isNil: return 990 | let c = currentContext() 991 | let gl = c.gl 992 | if shader == invalidProgram: 993 | createBuffers() 994 | shader = gl.newShaderProgram(vs, fs, [(0.GLuint, "aPosition"), (1.GLuint, "aTexCoord")]) 995 | gl.useProgram(shader) 996 | gl.enableVertexAttribArray(0) 997 | gl.uniformMatrix4fv(gl.getUniformLocation(shader, "uModelViewProjectionMatrix"), false, c.transform) 998 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer) 999 | gl.vertexAttribPointer(0, 3, cGL_FLOAT, true, 0, 0) 1000 | gl.enableVertexAttribArray(1) 1001 | gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer) 1002 | 1003 | var texCoords: array[4, GLfloat] 1004 | let t = m.group.getTextureQuad(gl, texCoords) 1005 | 1006 | gl.activeTexture(gl.TEXTURE0) 1007 | gl.uniform1i(gl.getUniformLocation(shader, "texUnit"), 0) 1008 | gl.bindTexture(gl.TEXTURE_2D, t) 1009 | 1010 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer) 1011 | 1012 | gl.enable(gl.DEPTH_TEST) 1013 | 1014 | let loc = gl.getUniformLocation(shader, "uPosOffset") 1015 | 1016 | for k, v in m.shown: 1017 | let kk = k 1018 | gl.uniform3iv(loc, kk) 1019 | gl.vertexAttribPointer(1, 2, cGL_FLOAT, false, 0, v.offsetInUVBuffer) 1020 | gl.drawElements(gl.TRIANGLES, 6 * 6, gl.UNSIGNED_BYTE) 1021 | 1022 | gl.disable(gl.DEPTH_TEST) 1023 | 1024 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, invalidBuffer) 1025 | gl.bindBuffer(gl.ARRAY_BUFFER, invalidBuffer) 1026 | 1027 | proc startApplication() = 1028 | var mainWindow: Window 1029 | when defined(ios) or defined(android): 1030 | mainWindow = newFullscreenWindow() 1031 | else: 1032 | mainWindow = newWindow(newRect(40, 40, 800, 600)) 1033 | var mainView = GameView.new(mainWindow.bounds) 1034 | mainView.autoresizingMask = {afFlexibleWidth, afFlexibleHeight} 1035 | mainWindow.addSubview(mainView) 1036 | 1037 | mainView.rootNode = newNode() 1038 | let cn = mainView.rootNode.newChild("camera") 1039 | let cam = cn.component(Camera) # Create camera 1040 | cn.position = newVector3(0, 0, 5) 1041 | cam.zNear = 0.1 1042 | cam.zFar = 500 1043 | let worldNode = mainView.rootNode.newChild("world") 1044 | worldNode.setComponent "World", newComponentWithDrawProc(proc() = 1045 | mainView.model.drawModel() 1046 | ) 1047 | 1048 | #discard startEditingNodeInView(mainView.rootNode, mainView) 1049 | 1050 | let a = newAnimation() 1051 | a.numberOfLoops = -1 1052 | a.loopDuration = 1 1053 | 1054 | var lastTime = epochTime() 1055 | 1056 | a.onAnimate = proc(p: float) = 1057 | let t = epochTime() 1058 | mainView.update(t - lastTime) 1059 | lastTime = t 1060 | 1061 | mainWindow.addAnimation(a) 1062 | 1063 | # Hide the mouse cursor and prevent the mouse from leaving the window. 1064 | # window.set_exclusive_mouse(True) 1065 | 1066 | runApplication: 1067 | startApplication() 1068 | --------------------------------------------------------------------------------