fenomas / noa

Experimental voxel game engine.
MIT License
616 stars 91 forks source link

Resetting and saving the state of the world #13

Closed Nesh108 closed 7 years ago

Nesh108 commented 7 years ago

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.

fenomas commented 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

fenomas commented 7 years ago

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:

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!

Nesh108 commented 7 years ago

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?

fenomas commented 7 years ago

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).

Nesh108 commented 7 years ago

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.

fenomas commented 7 years ago

Reopening this to track the invalidateAllChunks feature, until it's ready.. :grin:

Nesh108 commented 7 years ago

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:

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?

fenomas commented 7 years ago

Sorry, I really don't follow what you're asking. In:

noa.world.on('worldDataNeeded', function (id, data, x, y, z) { .... })

In 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.

Nesh108 commented 7 years ago

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?

fenomas commented 7 years ago

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.

Nesh108 commented 7 years ago

Ohhh, that's the height map doing the hills. Sorry, my bad! :D

Thanks again for your awesome support btw. I really appreciate :)

fenomas commented 7 years ago

This fix is now merged into master as v0.21.0.

Thanks for your feedback @Nesh108 !