1 module minermain; 2 3 import dlangui; 4 import dlangui.graphics.scene.scene3d; 5 import dlangui.graphics.scene.camera; 6 import dlangui.graphics.scene.mesh; 7 import dlangui.graphics.scene.material; 8 import dlangui.graphics.scene.effect; 9 import dlangui.graphics.scene.model; 10 import dlangui.graphics.scene.node; 11 import dlangui.graphics.scene.light; 12 import dlangui.graphics.scene.drawableobject; 13 import dlangui.graphics.scene.skybox; 14 import dlangui.graphics.scene.effect; 15 import dlangui.graphics.glsupport; 16 import dlangui.graphics.gldrawbuf; 17 18 //version = TEST_VISITOR_PERFORMANCE; 19 20 /* 21 version (Android) { 22 //enum SUPPORT_LEGACY_OPENGL = false; 23 public import EGL.eglplatform : EGLint; 24 public import EGL.egl; 25 //public import GLES2.gl2; 26 public import GLES3.gl3; 27 } else { 28 //enum SUPPORT_LEGACY_OPENGL = true; 29 import derelict.opengl3.gl3; 30 import derelict.opengl3.gl; 31 } 32 */ 33 34 import dminer.core.minetypes; 35 import dminer.core.blocks; 36 import dminer.core.world; 37 import dminer.core.generators; 38 import dminer.core.chunk; 39 40 mixin APP_ENTRY_POINT; 41 42 /// entry point for dlangui based application 43 extern (C) int UIAppMain(string[] args) { 44 // embed resources listed in views/resources.list into executable 45 embeddedResourceList.addResources(embedResourcesFromList!("resources.list")()); 46 //embeddedResourceList.dumpEmbeddedResources(); 47 48 debug { 49 testPlanes(); 50 } 51 52 // create window 53 Window window = Platform.instance.createWindow("DlangUI Voxel RPG", null, WindowFlag.Resizable, 600, 500); 54 window.mainWidget = new UiWidget(); 55 56 //MeshPart part = new MeshPart(); 57 58 // show window 59 window.show(); 60 61 // run message loop 62 return Platform.instance.enterMessageLoop(); 63 } 64 65 class ChunkVisitCounter : ChunkVisitor { 66 int count; 67 bool visit(World world, SmallChunk * chunk) { 68 count++; 69 return true; 70 } 71 } 72 73 class MinerDrawable : MaterialDrawableObject, ChunkVisitor { 74 75 import dlangui.graphics.scene.node; 76 private World _world; 77 private ChunkDiamondVisitor _chunkVisitor; 78 private VisibilityCheckIterator _chunkIterator; 79 private Vector3d _pos; 80 private Node3d _node; 81 private Camera _cam; 82 private vec3 _camPosition; 83 private vec3 _camForwardVector; 84 private bool _wireframe; 85 86 @property bool wireframe() { return _wireframe; } 87 @property void wireframe(bool flgWireframe) { _wireframe = flgWireframe; } 88 89 this(World world, Material material, Camera cam) { 90 super(material); 91 _world = world; 92 _cam = cam; 93 } 94 int _skippedCount; 95 int _drawnCount; 96 override void draw(Node3d node, bool wireframe) { 97 /// override it 98 _node = node; 99 //Log.d("drawing Miner scene"); 100 //_chunkVisitor.init(_world, MAX_VIEW_DISTANCE, this); 101 _pos = _world.camPosition.pos; 102 _camPosition = _cam.translation; 103 _camForwardVector = _cam.forwardVectorWorld; 104 //_camPosition -= _camForwardVector * 8; 105 _skippedCount = _drawnCount = 0; 106 long ts = currentTimeMillis(); 107 //_chunkVisitor.visitChunks(_pos); 108 Vector3d camVector; 109 camVector.x = cast(int)(_camForwardVector.x * 256); 110 camVector.y = cast(int)(_camForwardVector.y * 256); 111 camVector.z = cast(int)(_camForwardVector.z * 256); 112 version (TEST_VISITOR_PERFORMANCE) { 113 ChunkVisitCounter countVisitor = new ChunkVisitCounter(); 114 _chunkIterator.start(_world, _world.camPosition.pos, MAX_VIEW_DISTANCE); 115 _chunkIterator.visitVisibleChunks(countVisitor, camVector); 116 long durationNoDraw = currentTimeMillis() - ts; 117 _chunkIterator.start(_world, _world.camPosition.pos, MAX_VIEW_DISTANCE); 118 _chunkIterator.visitVisibleChunks(this, camVector); 119 long duration = currentTimeMillis() - ts; 120 Log.d("drawing of Miner scene finished in ", duration, " ms skipped:", _skippedCount, " drawn:", _drawnCount, " duration(noDraw)=", durationNoDraw); 121 } else { 122 _chunkIterator.start(_world, _world.camPosition.pos, MAX_VIEW_DISTANCE); 123 _chunkIterator.visitVisibleChunks(this, camVector); 124 long duration = currentTimeMillis() - ts; 125 Log.d("drawing of Miner scene finished in ", duration, " ms skipped:", _skippedCount, " drawn:", _drawnCount); 126 } 127 } 128 bool visit(World world, SmallChunk * chunk) { 129 if (chunk) { 130 Vector3d p = chunk.position; 131 vec3 chunkPos = vec3(p.x + 4, p.y + 4, p.z + 4); 132 float camDist = (_camPosition - chunkPos).length; 133 vec3 chunkDirection = (chunkPos - (_camPosition - (_camForwardVector * 12))).normalized; 134 float dot = _camForwardVector.dot(chunkDirection); 135 float threshold = 0.80; 136 if (camDist < 16) 137 threshold = 0.2; 138 //Log.d("visit() chunkPos ", chunkPos, " chunkDir ", chunkDirection, " camDir ", " dot ", dot, " threshold ", threshold); 139 140 if (dot < threshold) { // cos(45) 141 _skippedCount++; 142 return false; 143 } 144 Mesh mesh = chunk.getMesh(world); 145 if (mesh) { 146 _material.bind(_node, mesh, lights(_node)); 147 _material.drawMesh(mesh, _wireframe); 148 _material.unbind(); 149 _drawnCount++; 150 } 151 return true; 152 } 153 return true; 154 } 155 } 156 157 class UiWidget : VerticalLayout { //, CellVisitor 158 this() { 159 super("OpenGLView"); 160 layoutWidth = FILL_PARENT; 161 layoutHeight = FILL_PARENT; 162 alignment = Align.Center; 163 try { 164 parseML(q{ 165 { 166 margins: 0 167 padding: 0 168 //backgroundImageId: "tx_fabric.tiled" 169 backgroundColor: 0x000000; 170 layoutWidth: fill 171 layoutHeight: fill 172 173 VerticalLayout { 174 id: glView 175 margins: 0 176 padding: 0 177 layoutWidth: fill 178 layoutHeight: fill 179 TextWidget { text: "MinerD example"; textColor: "red"; fontSize: 150%; fontWeight: 800; fontFace: "Arial" } 180 VSpacer { layoutWeight: 30 } 181 TextWidget { id: lblPosition; text: ""; backgroundColor: 0x80202020; textColor: 0xFFE0E0 } 182 } 183 } 184 }, "", this); 185 } catch (Exception e) { 186 Log.e("Failed to parse dml", e); 187 } 188 // assign OpenGL drawable to child widget background 189 childById("glView").backgroundDrawable = DrawableRef(new OpenGLDrawable(&doDraw)); 190 191 _scene = new Scene3d(); 192 193 _cam = new Camera(); 194 _cam.translate(vec3(0, 14, -7)); 195 196 _scene.activeCamera = _cam; 197 198 static if (true) { 199 _scene.skyBox.setFaceTexture(SkyBox.Face.Right, "skybox_night_right1"); 200 _scene.skyBox.setFaceTexture(SkyBox.Face.Left, "skybox_night_left2"); 201 _scene.skyBox.setFaceTexture(SkyBox.Face.Top, "skybox_night_top3"); 202 _scene.skyBox.setFaceTexture(SkyBox.Face.Bottom, "skybox_night_bottom4"); 203 _scene.skyBox.setFaceTexture(SkyBox.Face.Front, "skybox_night_front5"); 204 _scene.skyBox.setFaceTexture(SkyBox.Face.Back, "skybox_night_back6"); 205 } else { 206 _scene.skyBox.setFaceTexture(SkyBox.Face.Right, "debug_right"); 207 _scene.skyBox.setFaceTexture(SkyBox.Face.Left, "debug_left"); 208 _scene.skyBox.setFaceTexture(SkyBox.Face.Top, "debug_top"); 209 _scene.skyBox.setFaceTexture(SkyBox.Face.Bottom, "debug_bottom"); 210 _scene.skyBox.setFaceTexture(SkyBox.Face.Front, "debug_front"); 211 _scene.skyBox.setFaceTexture(SkyBox.Face.Back, "debug_back"); 212 } 213 214 dirLightNode = new Node3d(); 215 dirLightNode.rotateY(-15); 216 dirLightNode.translateX(2); 217 dirLightNode.translateY(3); 218 dirLightNode.translateZ(0); 219 dirLightNode.light = Light.createPoint(vec3(1.0, 1.0, 1.0), 55); //Light.createDirectional(vec3(1, 0.5, 0.5)); 220 //dirLightNode.light = Light.createDirectional(vec3(1, 0.5, 0.5)); 221 dirLightNode.light.enabled = true; 222 _scene.addChild(dirLightNode); 223 224 225 int x0 = 0; 226 int y0 = 0; 227 int z0 = 0; 228 229 230 _minerMesh = new Mesh(VertexFormat(VertexElementType.POSITION, VertexElementType.NORMAL, VertexElementType.COLOR, VertexElementType.TEXCOORD0)); 231 _world = new World(); 232 233 initWorldTerrain(_world); 234 235 int cy0 = 3; 236 for (int y = CHUNK_DY - 1; y > 0; y--) 237 if (!_world.canPass(Vector3d(0, y, 0))) { 238 cy0 = y; 239 break; 240 } 241 _world.camPosition = Position(Vector3d(0, cy0, 0), Vector3d(0, 0, 1)); 242 243 _world.setCell(5, cy0 + 5, 7, BlockId.face_test); 244 _world.setCell(-5, cy0 + 5, 7, BlockId.face_test); 245 _world.setCell(5, cy0 + 5, -7, BlockId.face_test); 246 _world.setCell(3, cy0 + 5, 13, BlockId.face_test); 247 248 249 //_world.makeCastleWall(Vector3d(25, cy0 - 5, 12), Vector3d(1, 0, 0), 12, 30, 4, BlockId.brick); 250 _world.makeCastle(Vector3d(0, cy0, 60), 30, 12); 251 252 updateCamPosition(false); 253 //updateMinerMesh(); 254 255 Material minerMaterial = new Material(EffectId("textured.vert", "textured.frag", null), "blocks"); 256 //Material minerMaterial = new Material(EffectId("colored.vert", "colored.frag", null), "blocks"); 257 minerMaterial.ambientColor = vec3(0.25,0.25,0.25); 258 minerMaterial.textureLinear = false; 259 minerMaterial.fogParams = new FogParams(vec4(0.01, 0.01, 0.01, 1), 12, 80); 260 //minerMaterial.specular = 10; 261 _minerDrawable = new MinerDrawable(_world, minerMaterial, _cam); 262 //_minerDrawable.autobindLights = false; 263 //Model minerDrawable = new Model(minerMaterial, _minerMesh); 264 Node3d minerNode = new Node3d("miner", _minerDrawable); 265 //_minerDrawable.wireframe = true; 266 _scene.addChild(minerNode); 267 268 269 focusable = true; 270 } 271 272 MinerDrawable _minerDrawable; 273 274 int lastMouseX; 275 int lastMouseY; 276 /// process key event, return true if event is processed. 277 override bool onMouseEvent(MouseEvent event) { 278 if (event.action == MouseAction.ButtonDown) { 279 lastMouseX = event.x; 280 lastMouseY = event.y; 281 if (event.button == MouseButton.Left && false) { 282 int x = event.x; 283 int y = event.y; 284 int xindex = 0; 285 if (x > width * 2 / 3) 286 xindex = 2; 287 else if (x > width * 1 / 3) 288 xindex = 1; 289 int yindex = 0; 290 if (y > height * 2 / 3) 291 yindex = 2; 292 else if (y > height * 1 / 3) 293 yindex = 1; 294 int index = yindex * 3 + xindex; 295 /* 296 index: 297 0 1 2 298 3 4 5 299 6 7 8 300 */ 301 switch(index) { 302 default: 303 case 1: 304 case 4: 305 //_world.camPosition.forward(1); 306 //updateCamPosition(); 307 startMoveAnimation(_world.camPosition.direction.forward); 308 break; 309 case 0: 310 case 3: 311 _world.camPosition.turnLeft(); 312 updateCamPosition(); 313 break; 314 case 2: 315 case 5: 316 _world.camPosition.turnRight(); 317 updateCamPosition(); 318 break; 319 case 7: 320 //_world.camPosition.backward(1); 321 //updateCamPosition(); 322 startMoveAnimation(-_world.camPosition.direction.forward); 323 break; 324 case 6: 325 //_world.camPosition.moveLeft(); 326 //updateCamPosition(); 327 startMoveAnimation(_world.camPosition.direction.left); 328 break; 329 case 8: 330 //_world.camPosition.moveRight(); 331 //updateCamPosition(); 332 startMoveAnimation(_world.camPosition.direction.right); 333 break; 334 } 335 } 336 } else if (event.action == MouseAction.Move) { 337 if (event.lbutton.isDown) { 338 int deltaX = event.x - lastMouseX; 339 int deltaY = event.y - lastMouseY; 340 int maxshift = width > 100 ? width : 100; 341 float deltaAngleX = deltaX * 45.0f / maxshift; 342 float deltaAngleY = deltaY * 45.0f / maxshift; 343 lastMouseX = event.x; 344 lastMouseY = event.y; 345 float newAngle = _angle + deltaAngleX; 346 if (newAngle < -180) 347 newAngle += 360; 348 else if (newAngle > 180) 349 newAngle -= 360; 350 setAngle(newAngle, true); 351 float newAngleY = _yAngle + deltaAngleY; 352 if (newAngleY < -65) 353 newAngleY = -65; 354 else if (newAngleY > 65) 355 newAngleY = 65; 356 setYAngle(newAngleY, true); 357 } 358 } else if (event.action == MouseAction.ButtonUp || event.action == MouseAction.Cancel) { 359 stopMoveAnimation(); 360 } 361 return true; 362 } 363 364 /// process key event, return true if event is processed. 365 override bool onKeyEvent(KeyEvent event) { 366 if (event.action == KeyAction.KeyDown) { 367 switch(event.keyCode) with(KeyCode) { 368 case F1: 369 _minerDrawable.wireframe = !_minerDrawable.wireframe; 370 return true; 371 case KEY_W: 372 case UP: 373 _world.camPosition.forward(1); 374 updateCamPosition(); 375 return true; 376 case DOWN: 377 case KEY_S: 378 _world.camPosition.backward(1); 379 updateCamPosition(); 380 return true; 381 case KEY_A: 382 case LEFT: 383 _world.camPosition.turnLeft(); 384 updateCamPosition(); 385 return true; 386 case KEY_D: 387 case RIGHT: 388 _world.camPosition.turnRight(); 389 updateCamPosition(); 390 return true; 391 case HOME: 392 case KEY_E: 393 _world.camPosition.moveUp(); 394 updateCamPosition(); 395 return true; 396 case END: 397 case KEY_Q: 398 _world.camPosition.moveDown(); 399 updateCamPosition(); 400 return true; 401 case KEY_Z: 402 _world.camPosition.moveLeft(); 403 updateCamPosition(); 404 return true; 405 case KEY_C: 406 _world.camPosition.moveRight(); 407 updateCamPosition(); 408 return true; 409 case KEY_F: 410 flying = !flying; 411 if (!flying) 412 _world.camPosition.pos.y = CHUNK_DY - 3; 413 updateCamPosition(); 414 return true; 415 case KEY_U: 416 enableMeshUpdate = !enableMeshUpdate; 417 updateCamPosition(); 418 return true; 419 default: 420 return false; 421 } 422 } 423 return false; 424 } 425 426 Node3d dirLightNode; 427 428 //void visit(World world, ref Position camPosition, Vector3d pos, cell_t cell, int visibleFaces) { 429 // BlockDef def = BLOCK_DEFS[cell]; 430 // def.createFaces(world, world.camPosition, pos, visibleFaces, _minerMesh); 431 //} 432 433 bool flying = false; 434 bool enableMeshUpdate = true; 435 Vector3d _moveAnimationDirection; 436 437 void animateMoving() { 438 if (_moveAnimationDirection != Vector3d(0,0,0)) { 439 Vector3d animPos = _world.camPosition.pos + _moveAnimationDirection; 440 vec3 p = vec3(animPos.x + 0.5f, animPos.y + 0.5f, animPos.z + 0.5f); 441 if ((_animatingPosition - p).length < 2) { 442 _world.camPosition.pos += _moveAnimationDirection; 443 updateCamPosition(true); 444 } 445 } 446 } 447 448 void updateCamPosition(bool animateIt = true) { 449 import std.string; 450 import std.conv : to; 451 import std.utf : toUTF32; 452 import std.format; 453 454 if (!flying) { 455 animateMoving(); 456 while(_world.canPass(_world.camPosition.pos + Vector3d(0, -1, 0))) 457 _world.camPosition.pos += Vector3d(0, -1, 0); 458 if(!_world.canPass(_world.camPosition.pos + Vector3d(0, -1, 0))) { 459 if (_world.canPass(_world.camPosition.pos + Vector3d(0, 1, 0))) 460 _world.camPosition.pos += Vector3d(0, 1, 0); 461 else if (_world.canPass(_world.camPosition.pos + Vector3d(1, 0, 0))) 462 _world.camPosition.pos += Vector3d(1, 0, 0); 463 else if (_world.canPass(_world.camPosition.pos + Vector3d(-1, 0, 0))) 464 _world.camPosition.pos += Vector3d(-1, 0, 0); 465 else if (_world.canPass(_world.camPosition.pos + Vector3d(0, 0, 1))) 466 _world.camPosition.pos += Vector3d(0, 0, 1); 467 else if (_world.canPass(_world.camPosition.pos + Vector3d(0, 0, -1))) 468 _world.camPosition.pos += Vector3d(0, 0, -1); 469 while(_world.canPass(_world.camPosition.pos + Vector3d(0, -1, 0))) 470 _world.camPosition.pos += Vector3d(0, -1, 0); 471 } 472 } 473 474 setPos(vec3(_world.camPosition.pos.x + 0.5f, _world.camPosition.pos.y + 0.5f, _world.camPosition.pos.z + 0.5f), animateIt); 475 setAngle(_world.camPosition.direction.angle, animateIt); 476 477 updatePositionMessage(); 478 } 479 480 void updatePositionMessage() { 481 import std.string : format; 482 Widget w = childById("lblPosition"); 483 string dir = _world.camPosition.direction.dir.to!string; 484 dstring s = format("pos(%d,%d) h=%d fps:%d %s [F]lying: %s [U]pdateMesh: %s [F1] wireframe: %s", _world.camPosition.pos.x, _world.camPosition.pos.z, _world.camPosition.pos.y, 485 _fps, 486 dir, 487 flying, 488 enableMeshUpdate, 489 _minerDrawable ? _minerDrawable.wireframe : false 490 ).toUTF32; 491 w.text = s; 492 } 493 494 int _fps = 0; 495 496 void startMoveAnimation(Vector3d direction) { 497 _moveAnimationDirection = direction; 498 updateCamPosition(); 499 } 500 501 void stopMoveAnimation() { 502 _moveAnimationDirection = Vector3d(0, 0, 0); 503 updateCamPosition(); 504 } 505 506 //void updateMinerMesh() { 507 // _minerMesh.reset(); 508 // long ts = currentTimeMillis; 509 // _world.visitVisibleCells(_world.camPosition, this); 510 // long duration = currentTimeMillis - ts; 511 // Log.d("DiamondVisitor finished in ", duration, " ms ", "Vertex count: ", _minerMesh.vertexCount); 512 // 513 // invalidate(); 514 // //for (int i = 0; i < 20; i++) 515 // // Log.d("vertex: ", _minerMesh.vertex(i)); 516 //} 517 518 World _world; 519 vec3 _position; 520 float _directionAngle = 0; 521 float _yAngle = -15; 522 float _angle; 523 vec3 _animatingPosition; 524 float _animatingAngle; 525 float _animatingYAngle; 526 527 void setPos(vec3 newPos, bool animateIt = false) { 528 if (animateIt) { 529 _position = newPos; 530 } else { 531 _animatingPosition = newPos; 532 _position = newPos; 533 } 534 } 535 536 void setAngle(float newAngle, bool animateIt = false) { 537 if (animateIt) { 538 _angle = newAngle; 539 } else { 540 _animatingAngle = newAngle; 541 _angle = newAngle; 542 } 543 } 544 545 void setYAngle(float newAngle, bool animateIt = false) { 546 if (animateIt) { 547 _yAngle = newAngle; 548 } else { 549 _animatingYAngle = newAngle; 550 _yAngle = newAngle; 551 } 552 } 553 554 /// returns true is widget is being animated - need to call animate() and redraw 555 @property override bool animating() { return true; } 556 /// animates window; interval is time left from previous draw, in hnsecs (1/10000000 of second) 557 override void animate(long interval) { 558 //Log.d("animating"); 559 if (interval > 0) { 560 int newfps = cast(int)(10000000.0 / interval); 561 if (newfps < _fps - 3 || newfps > _fps + 3) { 562 _fps = newfps; 563 updatePositionMessage(); 564 } 565 } 566 animateMoving(); 567 if (_animatingAngle != _angle) { 568 float delta = _angle - _animatingAngle; 569 if (delta > 180) 570 delta -= 360; 571 else if (delta < -180) 572 delta += 360; 573 float dist = delta < 0 ? -delta : delta; 574 if (dist < 5) { 575 _animatingAngle = _angle; 576 } else { 577 float speed = 360 / 2; 578 float step = speed * interval / 10000000.0f; 579 //Log.d("Rotate animation delta=", delta, " dist=", dist, " elapsed=", interval, " step=", step); 580 if (step > dist) 581 step = dist; 582 delta = delta * (step /dist); 583 _animatingAngle += delta; 584 } 585 } 586 if (_animatingYAngle != _yAngle) { 587 float delta = _yAngle - _animatingYAngle; 588 if (delta > 180) 589 delta -= 360; 590 else if (delta < -180) 591 delta += 360; 592 float dist = delta < 0 ? -delta : delta; 593 if (dist < 5) { 594 _animatingYAngle = _yAngle; 595 } else { 596 float speed = 360 / 2; 597 float step = speed * interval / 10000000.0f; 598 //Log.d("Rotate animation delta=", delta, " dist=", dist, " elapsed=", interval, " step=", step); 599 if (step > dist) 600 step = dist; 601 delta = delta * (step /dist); 602 _animatingYAngle += delta; 603 } 604 } 605 if (_animatingPosition != _position) { 606 vec3 delta = _position - _animatingPosition; 607 float dist = delta.length; 608 if (dist < 0.01) { 609 _animatingPosition = _position; 610 // done 611 } else { 612 float speed = 8; 613 if (dist > 2) 614 speed = (dist - 2) * 3 + speed; 615 float step = speed * interval / 10000000.0f; 616 //Log.d("Move animation delta=", delta, " dist=", dist, " elapsed=", interval, " step=", step); 617 if (step > dist) 618 step = dist; 619 delta = delta * (step / dist); 620 _animatingPosition += delta; 621 } 622 } 623 invalidate(); 624 } 625 float angle = 0; 626 627 Scene3d _scene; 628 Camera _cam; 629 Mesh _minerMesh; 630 631 632 /// this is OpenGLDrawableDelegate implementation 633 private void doDraw(Rect windowRect, Rect rc) { 634 _cam.setPerspective(rc.width, rc.height, 45.0f, 0.3, MAX_VIEW_DISTANCE); 635 _cam.setIdentity(); 636 _cam.translate(_animatingPosition); 637 _cam.rotateY(_animatingAngle); 638 _cam.rotateX(_yAngle); 639 640 641 dirLightNode.setIdentity(); 642 dirLightNode.translate(_animatingPosition); 643 dirLightNode.rotateY(_animatingAngle); 644 645 checkgl!glEnable(GL_CULL_FACE); 646 //checkgl!glDisable(GL_CULL_FACE); 647 checkgl!glEnable(GL_DEPTH_TEST); 648 checkgl!glCullFace(GL_BACK); 649 650 Log.d("Drawing position ", _animatingPosition, " angle ", _animatingAngle); 651 652 _scene.drawScene(false); 653 654 checkgl!glDisable(GL_DEPTH_TEST); 655 checkgl!glDisable(GL_CULL_FACE); 656 } 657 658 ~this() { 659 destroy(_scene); 660 destroy(_world); 661 } 662 }