Closed Nesh108 closed 7 years ago
Hey,
For the first point, this is an API the engine needs, but let me think a little about the right way to do it. The easiest way would, in effect, be to flag all the chunks as needing replacement, but still use the current (gradual) system of unloading/reloading them. The effect would be that the chunks would unload and reload (with different data, assuming the client provides it) one by one over a number of frames, rather than all at once, to avoid locking up the JS engine. Does that cover your needs? If you want to hide the gradual unloading from the player, you could always tick the engine several times without rendering.
As for a world[x][y][z]
, currently there's no exposed way of doing this, because chunks are meant to be an internal abstraction - if you start using them directly you'll need to take into account some implementation details that may change in the future. But if you want to do it anyway to hack on it, here's how you'd currently do it:
noa.world._chunkHash.get(i, j, k)
where i, j, k
are the chunk's coordinates, modulo'd to the range 0 .. 1023
. The relevant internal accessor is here:
https://github.com/andyhall/noa/blob/master/lib/world.js#L215-L220
Hi, so I have taken a stab at implementing this. I have it working in my project, but it would be great if you can try it in yours and make sure it's suiting your needs.
First, to get this version please pull the repo and switch to the develop
branch. You'll need to require the engine locally, as the develop branch is not on npm. I added a minimal implementation of swapping worlds to the testbed - just run npm test
from inside the repo, and hit the "O" key in the test world.
As for the changes, there are two main ones:
noa.world.invalidateAllChunks()
method. It basically internally marks all chunks as needing to be reloaded, so the engine will dispose them and request new data from the client.noa.world.setChunkData
and the chunkBeingRemoved
event it emits now have a userData
property. You can set this property to any value, and that value will be emitted when the chunk is removed.The idea is that together, these should make it possible to (a) dynamically change worlds and (b) serialize both sets of data separately, without the client game needing to directly access chunks, or need to understand their internal structure, etc. When you set a chunks data, just attach userData specifying what kind of world data it is, and if that chunk is removed (whether because the user moved out of range or because the world data was invalidated) you can collect the chunk's data and store it somewhere according to the userData if you want.
That way, you simply call invalidateAllChunks()
when you want the game to recreate all the chunks it knows about. You'll then get a bunch of chunkBeingRemoved
events, but they can be handled normally, and a bunch of worldDataNeeded
events which should also be handled normally (but according to your desired world data). Attaching userData at both ends lets you store or otherwise handle the sets of data separately, since the unload events will occur over a number of frames after you invalidate the world.
A rough (pseudocode) implementation of a game with two worlds, and some way of serializing them both, might look like this:
var currWorld = "world1"
// provide world data - either newly created, or retrieved from storage
noa.world.on('worldDataNeeded', function (id, data, x, y, z) {
if (myStorageHas( currWorld, id )) {
// fill "data" with world data from storage for the current world
} else {
// fill "data" with newly created data for the current world
}
// "data" ready to pass back to noa - set userData to the current world name
var userData = currWorld
noa.world.setChunkData(id, data, userData)
})
// handler for when a chunk is about to be unloaded
noa.world.on( 'chunkBeingRemoved', function(id, data, userData) {
// store this chunk of data for later retrieval
// the userData will be "world1" or "world2", as passed in when the data was first set
})
// change the current world
function setWorld(name) {
currWorld = name
noa.world.invalidateAllChunks()
}
That's just an outline but hopefully it makes the idea clear of how the engine expects serialization to happen.
Please try this out and let me know if you see any problems!
It worked great! Thanks :)
The only problem now is that if the player had dug a bit, it will get stuck under the new terrain.
I tried re-positioning the player with:
this.player_mesh.position = new BABYLON.Vector3(0,20,0);
But nothing happens. Is that the correct way to go?
Thanks for checking!
To move the player, do something like:
noa.entities.getPositionData(noa.playerEntity).setPosition(x, y, z)
I really should write up some kind of documentation about the big-picture assumptions of this library, but in broad strokes, it tries to expose most of the interesting features of the world as either APIs on noa
or its libraries, or else as component state managed by the entities
lib.
Anyway suffice to say that the player's "real" position is the one in its position
component, and the engine will automatically move an entity's mesh to match that position each render (this is handled inside the mesh
component).
I just made a pull request for the reset function, since I think it would be nice to have on the engine itself.
It just reset the player to the position specified in the opts.
Regarding accessing the block configuration of the map, I will need to see how to do it. Because the world would really need some way to save its state. I will add a new issue for that specifically.
Reopening this to track the invalidateAllChunks
feature, until it's ready.. :grin:
As an extension to this:
I am trying to change the shape of the world. Currently I am using this:
this.noa.world.on('worldDataNeeded', function (id, data, x, y, z) {
let get_new_material = this.terrains[this.selected_terrain];
// // populate ndarray with world data (block IDs or 0 for air)
if (Math.max(Math.abs(x), Math.abs(y), Math.abs(z)) <= Engine.MAX_N_BLOCKS) {
for (let i = 0; i < data.shape[0]; ++i) {
for (let k = 0; k < data.shape[2]; ++k) {
let height = get_height_map(x + i, z + k);
for (let j = 0; j < data.shape[1]; ++j) {
if (y + j < height) {
data.set(i, j, k, get_new_material(x, y + j, z));
}
}
}
}
}
// pass the finished data back to the game engine
this.noa.world.setChunkData(id, data);
}.bind(this));
I also tried removing that bit and replacing it with:
let get_new_material = this.terrains[this.selected_terrain];
for (let x = -Engine.MAX_N_BLOCKS; x <= Engine.MAX_N_BLOCKS; ++x) {
for (let y = -Engine.MAX_N_BLOCKS; y <= Engine.MAX_N_BLOCKS; ++y) {
for (let z = -Engine.MAX_N_BLOCKS; z <= Engine.MAX_N_BLOCKS; ++z) {
this.noa.addBlock(get_new_material(x, y, z), x, y, z);
}
}
}
But nothing seemed to get loaded :/
What I would need is the following:
worldDataNeeded
also for loading previously saved maps.Or do you say to drop the worldDataNeeded and just load/generate my maps completely on startup? That might slow the whole world down, right?
Sorry, I really don't follow what you're asking. In:
noa.world.on('worldDataNeeded', function (id, data, x, y, z) { .... })
id
is a unique (string) identifier for the requested chunk - you might use this to track whether the chunk's been created before, for exampledata
is a 3D ndarray of data (i.e. block ID values) for the chunkdata.shape
is just the size of the 3D array on each axisx, y, z
are the world coordinates of the requested chunk's lowest cornerIn other words, the engine is handing you a cube of world data, and telling you where the cube is and how big it is, and asking you to populate that cube with block ID values. And when you call data.set(i, j, k, someID)
, then i, j, k
are local coordinates within the chunk, so you're effectively initializing the ID value of the voxel at world coordinates x+i, y+j, z+k
.
Also, noa.addBlock()
is a completely different thing - it's for changing voxels in parts of the world that are already loaded and initialized. The worldDataNeeded
event is where you initialize chunks that don't exist in the engine yet.
Mmm, ok, I think I get it.
But from what I noticed, shape seems to contain a specific mountain/hill shape. So, if I go through each shape array, I will get just a world with the same shape but different blocks.
So, my world, regardless of what rules I give, will also look like your world, a bunch or small hills, just with different blocks. Isn't that the case for you?
I think you've misunderstood the sample code. data.shape
is the size of the 3D array, which currently is always equal to noa's chunkSize
setting plus two. So if your chunk size is 32 then data.shape
will be [34, 34, 34]
. It's just a set of sizes to iterate over, it has nothing to do with the world data.
The hills in my demo come from a heightmap function. For each x, z
coordinate it generates some height
value, and then it fills in block IDs for each voxel location where y+j < height
. Please take another look.
Ohhh, that's the height map doing the hills. Sorry, my bad! :D
Thanks again for your awesome support btw. I really appreciate :)
This fix is now merged into master as v0.21.0.
Thanks for your feedback @Nesh108 !
Hello!
I am trying to find a way to reset the world. Basically, I would like to be able to change the configuration of the world whenever I want.
I tried emitting 'worldDataNeeded' but it seems a bit overly complex. Is there a way to just invalidate the current chuck and update every block again?
Also, is there a way to retrieve the current state of the world? Something like a matrix world[x][y][z] that can be saved and restored.