Added support functions to help setting up basic multiplayer with `VoxelTerrain`.

These might change in the future. Will have to test that later,
when Godot 4 gets more stable.
master
Marc Gilleron 2022-01-31 21:23:39 +00:00
parent 1631e186b5
commit d1250ef0ad
17 changed files with 588 additions and 62 deletions

View File

@ -16,6 +16,7 @@ Godot 4 is required from this version.
- General
- Added `gi_mode` to terrain nodes to choose how they behave with Godot's global illumination
- Added `FastNoise2` for faster SIMD noise
- Added experimental support functions to help setting up basic multiplayer with `VoxelTerrain` (might change in the future)
- Smooth voxels
- `VoxelLodTerrain`: added *experimental* `full_load_mode`, in which all edited data is loaded at once, allowing any area to be edited anytime. Useful for some fixed-size volumes.

View File

@ -1,11 +1,52 @@
Multiplayer
=============
Multiplayer is a planned feature still in design.
!!! warn
Multiplayer is currently in experimental phase. It is possible to some extent, but may have issues to be improved and might even change completely in the future.
It is currently not possible to implement it efficiently with the script API, so C++ changes are required. Streams are also not suited for such a task because the use case is different. They require synchronous access, and networking is asynchronous.
!!! note
This page assumes you already have knowledge in general multiplayer programming. It is strongly recommended you learn it beforehand. You can have a look at [Godot's documentation page about networking](https://docs.godotengine.org/en/stable/tutorials/networking/index.html).
The plans are to implement a server-authoritative design, where clients don't stream anything by themselves, and rather listen to what the server sends them. The server will only send modified blocks, while the others can be generated locally.
It is important for the `VoxelViewer` system to be functional before this gets implemented, as it guarantees that voxel volumes can be streamed by multiple users at once.
In general, the plan is to expose APIs to implement a client/server architecture. There is no assumption of the networking library you use, so it could be Godot RPCs, Valve Networking Sockets or even TCP. There is however assumption that the server also runs a version of the game.
Server-side viewer with `VoxelTerrain`
--------------------------------------
There are some support functions you can use that can help putting together primitive multiplayer. The idea is for the server to be authoritative, and the client just receives information from it.
`VoxelTerrain` has a `Networking` category in the inspector. These properties are not necessarily specific to multiplayer, but were actually added to experiment with it, so they are grouped together.
Client and server will need a different setup.
### On the server
- Configure `VoxelTerrain` as normal, with a generator and maybe a stream.
- On `VoxelTerrain`, Enable `block_enter_notification_enabled`
- Add a script to `VoxelTerrain` implementing `func _on_data_block_entered(info)`. This function will be called each time a new voxel block enters a remote player's area. This will be a place where you may send the block to the client. You can use `VoxelBlockSerializer` to pack voxel data into bytes. The `info.are_voxels_edited()` boolean can tell if the block was ever edited: if it wasn't, you can avoid sending the whole data and just tell the client to generate the block locally.
- When a player joins, make sure a `VoxelViewer` is created for it, assign its `network_peer_id` and enable `requires_data_block_notifications`. This will make the terrain load blocks around it and notify when blocks need to be sent to the peer.
- On `VoxelTerrain`, enable `area_edit_notification_enabled`
- In your `VoxelTerrain` script, implement `func _on_area_edited(origin, size)`. This function will be called each time voxels are edited within a bounding box. Voxels inside may have to be sent to all players close enough. You can get a list of network peer IDs by calling `get_viewer_network_peer_ids_in_area(origin, size)`.
### On the client
- Configure `VoxelTerrain` with a mesher and maybe a generator, and turn off `automatic_loading_enabled`. Voxels will only load based on what the server sends.
- Add a script handling network messages. When a block is received from the server, store it inside `VoxelTerrain` by using the `try_set_block_data` function.
- When a box of edited voxels is received from the server, you may use a `VoxelTool` and the `paste` function to replace the edited voxels. If you want the client to generate the block locally, you could use the generator to make one with `generate_block_async()`. If you use asynchronous generation, note that blocks written with `try_set_block_data` will cancel blocks that are loading. That means if a client receives an edited block in the meantime, the generating block won't overwrite it.
- The client will still need a `VoxelViewer`, which will allow the terrain to detect when it can unload voxel data (the server does not send that information). To reduce the likelihood of "holes" in the terrain if blocks get unloaded too soon, you may give the `VoxelViewer` a larger view distance than the server.
- The client can have remote players synchronized so the player can see them, but you should not add a `VoxelViewer` to them (only the server does). The client should not have to stream terrain for remote players, it only has one for the local player.
With `VoxelLodTerrain`
------------------------
There is no support for now, but it is planned.
Protocol notes
---------------
RPCs in Godot use UDP (reliable or unreliable), so sending large amounts of voxels to clients could have limited speed. Instead, it would be an option to use TCP to send blocks instead, as well as large edits. Small edits or deterministic edits with ligthweight info could keep using reliable UDP. Problem: you would have to use two ports, one for UDP, one for TCP. So maybe it is a better idea to keep using reliable UDP.
Note: Minecraft's network protocol is entirely built on top of TCP.
RPCs in Godot use UDP, so sending large amounts of voxels to clients may be severely limited. Instead, it would be an option to use TCP in order to send blocks instead, as well as large edits. Small edits or deterministic edits could keep using UDP.

View File

@ -85,6 +85,7 @@ void register_voxel_types() {
ClassDB::register_virtual_class<VoxelInstanceLibraryItem>();
ClassDB::register_class<VoxelInstanceLibraryMultiMeshItem>();
ClassDB::register_class<VoxelInstanceLibrarySceneItem>();
ClassDB::register_class<VoxelDataBlockEnterInfo>();
// Storage
ClassDB::register_class<VoxelBuffer>();

View File

@ -540,6 +540,26 @@ bool VoxelServer::is_viewer_requiring_collisions(uint32_t viewer_id) const {
return viewer.require_collisions;
}
void VoxelServer::set_viewer_requires_data_block_notifications(uint32_t viewer_id, bool enabled) {
Viewer &viewer = _world.viewers.get(viewer_id);
viewer.requires_data_block_notifications = enabled;
}
bool VoxelServer::is_viewer_requiring_data_block_notifications(uint32_t viewer_id) const {
const Viewer &viewer = _world.viewers.get(viewer_id);
return viewer.requires_data_block_notifications;
}
void VoxelServer::set_viewer_network_peer_id(uint32_t viewer_id, int peer_id) {
Viewer &viewer = _world.viewers.get(viewer_id);
viewer.network_peer_id = peer_id;
}
int VoxelServer::get_viewer_network_peer_id(uint32_t viewer_id) const {
const Viewer &viewer = _world.viewers.get(viewer_id);
return viewer.network_peer_id;
}
bool VoxelServer::viewer_exists(uint32_t viewer_id) const {
return _world.viewers.is_valid(viewer_id);
}
@ -807,11 +827,11 @@ void VoxelServer::BlockDataRequest::apply_result() {
switch (type) {
case BlockDataRequest::TYPE_SAVE:
o.type = BlockDataOutput::TYPE_SAVE;
o.type = BlockDataOutput::TYPE_SAVED;
break;
case BlockDataRequest::TYPE_LOAD:
o.type = BlockDataOutput::TYPE_LOAD;
o.type = BlockDataOutput::TYPE_LOADED;
break;
default:
@ -950,7 +970,7 @@ void VoxelServer::BlockGenerateRequest::apply_result() {
o.position = position;
o.lod = lod;
o.dropped = !has_run;
o.type = BlockDataOutput::TYPE_LOAD;
o.type = BlockDataOutput::TYPE_GENERATED;
o.max_lod_hint = max_lod_hint;
o.initial_load = false;

View File

@ -36,8 +36,9 @@ public:
struct BlockDataOutput {
enum Type { //
TYPE_LOAD,
TYPE_SAVE
TYPE_LOADED,
TYPE_GENERATED,
TYPE_SAVED
};
Type type;
@ -83,6 +84,8 @@ public:
unsigned int view_distance = 128;
bool require_collisions = true;
bool require_visuals = true;
bool requires_data_block_notifications = false;
int network_peer_id = -1;
};
enum VolumeType { //
@ -130,6 +133,10 @@ public:
bool is_viewer_requiring_visuals(uint32_t viewer_id) const;
void set_viewer_requires_collisions(uint32_t viewer_id, bool enabled);
bool is_viewer_requiring_collisions(uint32_t viewer_id) const;
void set_viewer_requires_data_block_notifications(uint32_t viewer_id, bool enabled);
bool is_viewer_requiring_data_block_notifications(uint32_t viewer_id) const;
void set_viewer_network_peer_id(uint32_t viewer_id, int peer_id);
int get_viewer_network_peer_id(uint32_t viewer_id) const;
bool viewer_exists(uint32_t viewer_id) const;
template <typename F>

View File

@ -7,9 +7,12 @@
namespace zylann::voxel {
const char *VoxelBuffer::CHANNEL_ID_HINT_STRING = "Type,Sdf,Color,Indices,Weights,Data5,Data6,Data7";
static thread_local bool s_create_shared = false;
VoxelBuffer::VoxelBuffer() {
_buffer = gd_make_shared<VoxelBufferInternal>();
if (!s_create_shared) {
_buffer = gd_make_shared<VoxelBufferInternal>();
}
}
VoxelBuffer::VoxelBuffer(std::shared_ptr<VoxelBufferInternal> &other) {
@ -17,6 +20,15 @@ VoxelBuffer::VoxelBuffer(std::shared_ptr<VoxelBufferInternal> &other) {
_buffer = other;
}
Ref<VoxelBuffer> VoxelBuffer::create_shared(std::shared_ptr<VoxelBufferInternal> &other) {
Ref<VoxelBuffer> vb;
s_create_shared = true;
vb.instantiate();
s_create_shared = false;
vb->_buffer = other;
return vb;
}
VoxelBuffer::~VoxelBuffer() {}
void VoxelBuffer::clear() {

View File

@ -60,6 +60,9 @@ public:
~VoxelBuffer();
// Workaround because the constructor with arguments cannot always be used due to Godot limitations
static Ref<VoxelBuffer> create_shared(std::shared_ptr<VoxelBufferInternal> &other);
inline const VoxelBufferInternal &get_buffer() const {
#ifdef DEBUG_ENABLED
CRASH_COND(_buffer == nullptr);
@ -74,6 +77,13 @@ public:
return *_buffer;
}
inline std::shared_ptr<VoxelBufferInternal> get_buffer_shared() {
#ifdef DEBUG_ENABLED
CRASH_COND(_buffer == nullptr);
#endif
return _buffer;
}
//inline std::shared_ptr<VoxelBufferInternal> get_buffer_shared() { return _buffer; }
Vector3i get_size() const {
@ -167,7 +177,6 @@ private:
static void _bind_methods();
// Not sure yet if we'll really need shared_ptr or just no pointer
std::shared_ptr<VoxelBufferInternal> _buffer;
};

View File

@ -70,6 +70,14 @@ public:
return _needs_lodding;
}
inline void set_edited(bool edited) {
_edited = true;
}
inline bool is_edited() const {
return _edited;
}
private:
VoxelDataBlock(Vector3i bpos, std::shared_ptr<VoxelBufferInternal> &buffer, unsigned int p_lod_index) :
position(bpos), lod_index(p_lod_index), _voxels(buffer) {}
@ -82,6 +90,11 @@ private:
// Indicates if this block is different from the time it was loaded (should be saved)
bool _modified = false;
// Tells if the block has ever been edited.
// If `false`, the same data can be obtained by running the generator.
// Once it becomes `true`, it usually never comes back to `false` unless reverted.
bool _edited = false;
// Tells if it's worth requesting a more precise version of the data.
// Will be `true` if it's not worth it.
//bool _max_lod_hint = false;

View File

@ -0,0 +1,40 @@
#include "voxel_data_block_enter_info.h"
#include "../storage/voxel_buffer.h"
#include "../storage/voxel_data_block.h"
namespace zylann::voxel {
int VoxelDataBlockEnterInfo::_b_get_network_peer_id() const {
return network_peer_id;
}
Ref<VoxelBuffer> VoxelDataBlockEnterInfo::_b_get_voxels() const {
ERR_FAIL_COND_V(voxel_block == nullptr, Ref<VoxelBuffer>());
Ref<VoxelBuffer> vb = VoxelBuffer::create_shared(voxel_block->get_voxels_shared());
return vb;
}
Vector3i VoxelDataBlockEnterInfo::_b_get_position() const {
ERR_FAIL_COND_V(voxel_block == nullptr, Vector3i());
return voxel_block->position;
}
int VoxelDataBlockEnterInfo::_b_get_lod_index() const {
ERR_FAIL_COND_V(voxel_block == nullptr, 0);
return voxel_block->lod_index;
}
bool VoxelDataBlockEnterInfo::_b_are_voxels_edited() const {
ERR_FAIL_COND_V(voxel_block == nullptr, false);
return voxel_block->is_edited();
}
void VoxelDataBlockEnterInfo::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_network_peer_id"), &VoxelDataBlockEnterInfo::_b_get_network_peer_id);
ClassDB::bind_method(D_METHOD("get_voxels"), &VoxelDataBlockEnterInfo::_b_get_voxels);
ClassDB::bind_method(D_METHOD("get_position"), &VoxelDataBlockEnterInfo::_b_get_position);
ClassDB::bind_method(D_METHOD("get_lod_index"), &VoxelDataBlockEnterInfo::_b_get_lod_index);
ClassDB::bind_method(D_METHOD("are_voxels_edited"), &VoxelDataBlockEnterInfo::_b_are_voxels_edited);
}
} // namespace zylann::voxel

View File

@ -0,0 +1,33 @@
#ifndef VOXEL_DATA_BLOCK_ENTER_INFO_H
#define VOXEL_DATA_BLOCK_ENTER_INFO_H
#include <core/object/ref_counted.h>
namespace zylann::voxel {
class VoxelDataBlock;
class VoxelBuffer;
// Information sent with data block entering notifications.
// It is a class for script API convenience.
// You may neither create this object on your own, nor keep a reference to it.
class VoxelDataBlockEnterInfo : public Object {
GDCLASS(VoxelDataBlockEnterInfo, Object)
public:
int network_peer_id = -1;
VoxelDataBlock *voxel_block = nullptr;
private:
int _b_get_network_peer_id() const;
Ref<VoxelBuffer> _b_get_voxels() const;
Vector3i _b_get_position() const;
int _b_get_lod_index() const;
bool _b_are_voxels_edited() const;
//int _b_viewer_id() const;
static void _bind_methods();
};
} // namespace zylann::voxel
#endif // VOXEL_DATA_BLOCK_ENTER_INFO_H

View File

@ -1779,7 +1779,7 @@ void VoxelLodTerrain::process_octrees_fitting(Vector3 p_viewer_pos, std::vector<
void VoxelLodTerrain::apply_data_block_response(VoxelServer::BlockDataOutput &ob) {
VOXEL_PROFILE_SCOPE();
if (ob.type == VoxelServer::BlockDataOutput::TYPE_SAVE) {
if (ob.type == VoxelServer::BlockDataOutput::TYPE_SAVED) {
// That's a save confirmation event.
// Note: in the future, if blocks don't get copied before being sent for saving,
// we will need to use block versionning to know when we can reset the `modified` flag properly
@ -1838,6 +1838,7 @@ void VoxelLodTerrain::apply_data_block_response(VoxelServer::BlockDataOutput &ob
RWLockWrite wlock(data_lod.map_lock);
VoxelDataBlock *block = data_lod.map.set_block_buffer(ob.position, ob.voxels, false);
CRASH_COND(block == nullptr);
block->set_edited(ob.type == VoxelServer::BlockDataOutput::TYPE_LOADED);
}
if (_instancer != nullptr && ob.instances != nullptr) {

View File

@ -22,7 +22,7 @@ class VoxelInstancer;
// Paged terrain made of voxel blocks of variable level of detail.
// Designed for highest view distances, preferably using smooth voxels.
// Voxels are polygonized around the viewer by distance in a very large sphere, usually extending beyond far clip.
// Data is streamed using a VoxelStream, which must support LOD.
// VoxelStream and VoxelGenerator must support LOD.
class VoxelLodTerrain : public VoxelNode {
GDCLASS(VoxelLodTerrain, VoxelNode)
public:

View File

@ -5,10 +5,10 @@
#include "../server/voxel_server.h"
#include "../server/voxel_server_updater.h"
#include "../util/funcs.h"
#include "../util/godot/funcs.h"
#include "../util/macros.h"
#include "../util/profiling.h"
#include "../util/profiling_clock.h"
#include "voxel_data_block_enter_info.h"
#include <core/config/engine.h>
#include <core/core_string_names.h>
@ -297,6 +297,18 @@ Ref<VoxelBlockyLibrary> VoxelTerrain::get_voxel_library() const {
return Ref<VoxelBlockyLibrary>();
}
void VoxelTerrain::get_viewers_in_area(std::vector<int> &out_viewer_ids, Box3i voxel_box) const {
const Box3i block_box = voxel_box.downscaled(_data_map.get_block_size());
for (auto it = _paired_viewers.begin(); it != _paired_viewers.end(); ++it) {
const PairedViewer &viewer = *it;
if (viewer.state.data_box.intersects(block_box)) {
out_viewer_ids.push_back(viewer.id);
}
}
}
void VoxelTerrain::set_generate_collisions(bool enabled) {
_generate_collisions = enabled;
}
@ -343,6 +355,39 @@ void VoxelTerrain::set_max_view_distance(unsigned int distance_in_voxels) {
_max_view_distance_voxels = distance_in_voxels;
}
void VoxelTerrain::set_block_enter_notification_enabled(bool enable) {
_block_enter_notification_enabled = enable;
if (enable == false) {
const Vector3i *key = nullptr;
while ((key = _loading_blocks.next(key))) {
LoadingBlock *lb = _loading_blocks.getptr(*key);
CRASH_COND(lb == nullptr);
lb->viewers_to_notify.clear();
}
}
}
bool VoxelTerrain::is_block_enter_notification_enabled() const {
return _block_enter_notification_enabled;
}
void VoxelTerrain::set_area_edit_notification_enabled(bool enable) {
_area_edit_notification_enabled = enable;
}
bool VoxelTerrain::is_area_edit_notification_enabled() const {
return _area_edit_notification_enabled;
}
void VoxelTerrain::set_automatic_loading_enabled(bool enable) {
_automatic_loading_enabled = enable;
}
bool VoxelTerrain::is_automatic_loading_enabled() const {
return _automatic_loading_enabled;
}
void VoxelTerrain::set_material(unsigned int id, Ref<Material> material) {
// TODO Update existing block surfaces
ERR_FAIL_COND(id < 0 || id >= VoxelMesherBlocky::MAX_MATERIALS);
@ -388,7 +433,7 @@ void VoxelTerrain::try_schedule_mesh_update(VoxelMeshBlock *mesh_block) {
}
}
void VoxelTerrain::view_data_block(Vector3i bpos) {
void VoxelTerrain::view_data_block(Vector3i bpos, uint32_t viewer_id, bool require_notification) {
VoxelDataBlock *block = _data_map.get_block(bpos);
if (block == nullptr) {
@ -400,6 +445,10 @@ void VoxelTerrain::view_data_block(Vector3i bpos) {
LoadingBlock new_loading_block;
new_loading_block.viewers.add();
if (require_notification) {
new_loading_block.viewers_to_notify.push_back(viewer_id);
}
// Schedule a loading request
_loading_blocks.set(bpos, new_loading_block);
_blocks_pending_load.push_back(bpos);
@ -407,12 +456,20 @@ void VoxelTerrain::view_data_block(Vector3i bpos) {
} else {
// More viewers
loading_block->viewers.add();
if (require_notification) {
loading_block->viewers_to_notify.push_back(viewer_id);
}
}
} else {
// The block is loaded
block->viewers.add();
if (require_notification) {
notify_data_block_enter(*block, viewer_id);
}
// TODO viewers with varying flags during the game is not supported at the moment.
// They have to be re-created, which may cause world re-load...
}
@ -540,10 +597,14 @@ struct ScheduleSaveAction {
} // namespace
void VoxelTerrain::unload_data_block(Vector3i bpos) {
_data_map.remove_block(bpos, [this, bpos](VoxelDataBlock *block) {
const bool save = _stream.is_valid() && (!Engine::get_singleton()->is_editor_hint() || _run_stream_in_editor);
_data_map.remove_block(bpos, [this, bpos, save](VoxelDataBlock *block) {
emit_data_block_unloaded(block);
// Note: no need to copy the block because it gets removed from the map anyways
ScheduleSaveAction{ _blocks_to_save, false }(block);
if (save) {
// Note: no need to copy the block because it gets removed from the map anyways
ScheduleSaveAction{ _blocks_to_save, false }(block);
}
});
_loading_blocks.erase(bpos);
@ -632,6 +693,40 @@ void VoxelTerrain::remesh_all_blocks() {
});
}
// At the moment, this function is for client-side use case in multiplayer scenarios
void VoxelTerrain::generate_block_async(Vector3i block_position) {
if (_data_map.has_block(block_position)) {
// Already exists
return;
}
if (_loading_blocks.has(block_position)) {
// Already loading
return;
}
// if (require_notification) {
// new_loading_block.viewers_to_notify.push_back(viewer_id);
// }
LoadingBlock new_loading_block;
const Box3i block_box(_data_map.block_to_voxel(block_position), Vector3iUtil::create(_data_map.get_block_size()));
for (size_t i = 0; i < _paired_viewers.size(); ++i) {
const PairedViewer &viewer = _paired_viewers[i];
if (viewer.state.data_box.intersects(block_box)) {
new_loading_block.viewers.add();
}
}
if (new_loading_block.viewers.get() == 0) {
return;
}
// Schedule a loading request
// TODO This could also end up loading from stream
_loading_blocks.set(block_position, new_loading_block);
_blocks_pending_load.push_back(block_position);
}
void VoxelTerrain::start_streamer() {
VoxelServer::get_singleton()->set_volume_stream(_volume_id, _stream);
VoxelServer::get_singleton()->set_volume_generator(_volume_id, _generator);
@ -688,9 +783,14 @@ void VoxelTerrain::post_edit_area(Box3i box_in_voxels) {
// The edit can happen next to a boundary
if (block != nullptr) {
block->set_modified(true);
block->set_edited(true);
}
});
if (_area_edit_notification_enabled) {
GDVIRTUAL_CALL(_on_area_edited, box_in_voxels.pos, box_in_voxels.size);
}
try_schedule_mesh_update_from_data(box_in_voxels);
}
@ -825,6 +925,24 @@ bool VoxelTerrain::try_get_paired_viewer_index(uint32_t id, size_t &out_i) const
return false;
}
// TODO It is unclear yet if this API will stay. I have a feeling it might consume a lot of CPU
void VoxelTerrain::notify_data_block_enter(VoxelDataBlock &block, uint32_t viewer_id) {
if (!VoxelServer::get_singleton()->viewer_exists(viewer_id)) {
// The viewer might have been removed between the moment we requested the block and the moment we finished
// loading it
return;
}
if (_data_block_enter_info_obj == nullptr) {
_data_block_enter_info_obj = gd_make_unique<VoxelDataBlockEnterInfo>();
}
_data_block_enter_info_obj->network_peer_id = VoxelServer::get_singleton()->get_viewer_network_peer_id(viewer_id);
_data_block_enter_info_obj->voxel_block = &block;
if (!GDVIRTUAL_CALL(_on_data_block_enter, _data_block_enter_info_obj.get())) {
WARN_PRINT_ONCE("VoxelTerrain::notify_data_block_enter is unimplemented!");
}
}
void VoxelTerrain::_process() {
VOXEL_PROFILE_SCOPE();
process_viewers();
@ -936,11 +1054,11 @@ void VoxelTerrain::process_viewers() {
VoxelServer::get_singleton()->for_each_viewer(u);
}
const bool stream_enabled = (_stream.is_valid() || _generator.is_valid()) &&
const bool can_load_blocks = (_automatic_loading_enabled && (_stream.is_valid() || _generator.is_valid())) &&
(Engine::get_singleton()->is_editor_hint() == false || _run_stream_in_editor);
// Find out which blocks need to appear and which need to be unloaded
if (stream_enabled) {
{
VOXEL_PROFILE_SCOPE();
for (size_t i = 0; i < _paired_viewers.size(); ++i) {
@ -953,18 +1071,26 @@ void VoxelTerrain::process_viewers() {
if (prev_data_box != new_data_box) {
VOXEL_PROFILE_SCOPE();
const bool require_notifications = _block_enter_notification_enabled &&
VoxelServer::get_singleton()->is_viewer_requiring_data_block_notifications(viewer.id);
// Unview blocks that just fell out of range
prev_data_box.difference(new_data_box, [this, &viewer](Box3i out_of_range_box) {
out_of_range_box.for_each_cell([this, &viewer](Vector3i bpos) { unview_data_block(bpos); });
out_of_range_box.for_each_cell([this, &viewer](Vector3i bpos) { //
unview_data_block(bpos);
});
});
// View blocks that just entered the range
new_data_box.difference(prev_data_box, [this, &viewer](Box3i box_to_load) {
box_to_load.for_each_cell([this, &viewer](Vector3i bpos) {
// Load or update block
view_data_block(bpos);
});
});
if (can_load_blocks) {
new_data_box.difference(
prev_data_box, [this, &viewer, require_notifications](Box3i box_to_load) {
box_to_load.for_each_cell([this, &viewer, require_notifications](Vector3i bpos) {
// Load or update block
view_data_block(bpos, viewer.id, require_notifications);
});
});
}
}
}
@ -998,20 +1124,28 @@ void VoxelTerrain::process_viewers() {
if (viewer.state.requires_collisions != viewer.prev_state.requires_collisions) {
const Box3i box = new_mesh_box.clipped(prev_mesh_box);
if (viewer.state.requires_collisions) {
box.for_each_cell([this](Vector3i bpos) { view_mesh_block(bpos, false, true); });
box.for_each_cell([this](Vector3i bpos) { //
view_mesh_block(bpos, false, true);
});
} else {
box.for_each_cell([this](Vector3i bpos) { unview_mesh_block(bpos, false, true); });
box.for_each_cell([this](Vector3i bpos) { //
unview_mesh_block(bpos, false, true);
});
}
}
if (viewer.state.requires_meshes != viewer.prev_state.requires_meshes) {
const Box3i box = new_mesh_box.clipped(prev_mesh_box);
if (viewer.state.requires_meshes) {
box.for_each_cell([this](Vector3i bpos) { view_mesh_block(bpos, true, false); });
box.for_each_cell([this](Vector3i bpos) { //
view_mesh_block(bpos, true, false);
});
} else {
box.for_each_cell([this](Vector3i bpos) { unview_mesh_block(bpos, true, false); });
box.for_each_cell([this](Vector3i bpos) { //
unview_mesh_block(bpos, true, false);
});
}
}
}
@ -1030,7 +1164,7 @@ void VoxelTerrain::process_viewers() {
}
// It's possible the user didn't set a stream yet, or it is turned off
if (stream_enabled) {
if (can_load_blocks) {
send_block_data_requests();
}
@ -1040,35 +1174,20 @@ void VoxelTerrain::process_viewers() {
void VoxelTerrain::apply_data_block_response(VoxelServer::BlockDataOutput &ob) {
VOXEL_PROFILE_SCOPE();
const bool stream_enabled = (_stream.is_valid() || _generator.is_valid()) &&
(Engine::get_singleton()->is_editor_hint() == false || _run_stream_in_editor);
//print_line(String("Receiving {0} blocks").format(varray(output.emerged_blocks.size())));
if (ob.type == VoxelServer::BlockDataOutput::TYPE_SAVE) {
if (ob.type == VoxelServer::BlockDataOutput::TYPE_SAVED) {
if (ob.dropped) {
ERR_PRINT(String("Could not save block {0}").format(varray(ob.position)));
}
return;
}
CRASH_COND(ob.type != VoxelServer::BlockDataOutput::TYPE_LOAD);
CRASH_COND(ob.type != VoxelServer::BlockDataOutput::TYPE_LOADED &&
ob.type != VoxelServer::BlockDataOutput::TYPE_GENERATED);
const Vector3i block_pos = ob.position;
LoadingBlock loading_block;
{
LoadingBlock *loading_block_ptr = _loading_blocks.getptr(block_pos);
if (loading_block_ptr == nullptr) {
// That block was not requested or is no longer needed, drop it.
++_stats.dropped_block_loads;
return;
}
loading_block = *loading_block_ptr;
}
if (ob.dropped) {
// That block was cancelled by the server, but we are still expecting it.
// We'll have to request it again.
@ -1081,6 +1200,20 @@ void VoxelTerrain::apply_data_block_response(VoxelServer::BlockDataOutput &ob) {
return;
}
LoadingBlock loading_block;
{
LoadingBlock *loading_block_ptr = _loading_blocks.getptr(block_pos);
if (loading_block_ptr == nullptr) {
// That block was not requested or is no longer needed, drop it.
++_stats.dropped_block_loads;
return;
}
// Using move semantics because it can contain an allocated vector
loading_block = std::move(*loading_block_ptr);
}
// Now we got the block. If we still have to drop it, the cause will be an error.
_loading_blocks.erase(block_pos);
@ -1101,6 +1234,8 @@ void VoxelTerrain::apply_data_block_response(VoxelServer::BlockDataOutput &ob) {
const bool was_not_loaded = block == nullptr;
block = _data_map.set_block_buffer(block_pos, ob.voxels);
block->set_edited(ob.type == VoxelServer::BlockDataOutput::TYPE_LOADED);
if (was_not_loaded) {
// Set viewers count that are currently expecting the block
block->viewers = loading_block.viewers;
@ -1108,6 +1243,11 @@ void VoxelTerrain::apply_data_block_response(VoxelServer::BlockDataOutput &ob) {
emit_data_block_loaded(block);
for (unsigned int i = 0; i < loading_block.viewers_to_notify.size(); ++i) {
const uint32_t viewer_id = loading_block.viewers_to_notify[i];
notify_data_block_enter(*block, viewer_id);
}
// The block itself might not be suitable for meshing yet, but blocks surrounding it might be now
{
VOXEL_PROFILE_SCOPE();
@ -1116,9 +1256,58 @@ void VoxelTerrain::apply_data_block_response(VoxelServer::BlockDataOutput &ob) {
}
// We might have requested some blocks again (if we got a dropped one while we still need them)
if (stream_enabled) {
send_block_data_requests();
// if (stream_enabled) {
// send_block_data_requests();
// }
}
// Sets voxel data of a block, discarding existing data if any.
// If the given block coordinates are not inside any viewer's area, this function won't do anything and return false.
// If a block is already loading or generating at this position, it will be cancelled.
bool VoxelTerrain::try_set_block_data(Vector3i position, std::shared_ptr<VoxelBufferInternal> &voxel_data) {
VOXEL_PROFILE_SCOPE();
ERR_FAIL_COND_V(voxel_data == nullptr, false);
const Vector3i expected_block_size = Vector3iUtil::create(_data_map.get_block_size());
ERR_FAIL_COND_V_MSG(voxel_data->get_size() != expected_block_size, false,
String("Block size is different from expected size. "
"Expected {0}, got {1}")
.format(varray(expected_block_size, voxel_data->get_size())));
// Setup viewers count intersecting with this block
VoxelRefCount refcount;
for (unsigned int i = 0; i < _paired_viewers.size(); ++i) {
const PairedViewer &viewer = _paired_viewers[i];
if (viewer.state.data_box.contains(position)) {
refcount.add();
}
}
if (refcount.get() == 0) {
// Actually, this block is not even in range. So we may ignore it.
// If we don't want this behavior, we could introduce a fake viewer that adds a reference to all blocks in this
// volume as long as it is enabled?
return false;
}
// Cancel loading version if any
_loading_blocks.erase(position);
// Create or update block data
VoxelDataBlock *block = _data_map.set_block_buffer(position, voxel_data);
CRASH_COND(block == nullptr);
block->viewers = refcount;
// TODO How to set the `edited` flag? Does it matter in use cases for this function?
// The block itself might not be suitable for meshing yet, but blocks surrounding it might be now
try_schedule_mesh_update_from_data(
Box3i(_data_map.block_to_voxel(position), Vector3iUtil::create(get_data_block_size())));
return true;
}
bool VoxelTerrain::has_block(Vector3i position) const {
return _data_map.has_block(position);
}
void VoxelTerrain::process_meshing() {
@ -1355,6 +1544,27 @@ AABB VoxelTerrain::_b_get_bounds() const {
return AABB(b.pos, b.size);
}
bool VoxelTerrain::_b_try_set_block_data(Vector3i position, Ref<VoxelBuffer> voxel_data) {
ERR_FAIL_COND_V(voxel_data.is_null(), false);
return try_set_block_data(position, voxel_data->get_buffer_shared());
}
PackedInt32Array VoxelTerrain::_b_get_viewer_network_peer_ids_in_area(Vector3i area_origin, Vector3i area_size) const {
static thread_local std::vector<int> s_ids;
std::vector<int> &viewer_ids = s_ids;
viewer_ids.clear();
get_viewers_in_area(viewer_ids, Box3i(area_origin, area_size));
PackedInt32Array peer_ids;
peer_ids.resize(viewer_ids.size());
for (size_t i = 0; i < viewer_ids.size(); ++i) {
const int peer_id = VoxelServer::get_singleton()->get_viewer_network_peer_id(viewer_ids[i]);
peer_ids.write[i] = peer_id;
}
return peer_ids;
}
void VoxelTerrain::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_material", "id", "material"), &VoxelTerrain::set_material);
ClassDB::bind_method(D_METHOD("get_material", "id"), &VoxelTerrain::get_material);
@ -1362,6 +1572,16 @@ void VoxelTerrain::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_max_view_distance", "distance_in_voxels"), &VoxelTerrain::set_max_view_distance);
ClassDB::bind_method(D_METHOD("get_max_view_distance"), &VoxelTerrain::get_max_view_distance);
ClassDB::bind_method(D_METHOD("set_block_enter_notification_enabled", "enabled"),
&VoxelTerrain::set_block_enter_notification_enabled);
ClassDB::bind_method(
D_METHOD("is_block_enter_notification_enabled"), &VoxelTerrain::is_block_enter_notification_enabled);
ClassDB::bind_method(D_METHOD("set_area_edit_notification_enabled", "enabled"),
&VoxelTerrain::set_area_edit_notification_enabled);
ClassDB::bind_method(
D_METHOD("is_area_edit_notification_enabled"), &VoxelTerrain::is_area_edit_notification_enabled);
ClassDB::bind_method(D_METHOD("get_generate_collisions"), &VoxelTerrain::get_generate_collisions);
ClassDB::bind_method(D_METHOD("set_generate_collisions", "enabled"), &VoxelTerrain::set_generate_collisions);
@ -1391,11 +1611,24 @@ void VoxelTerrain::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_run_stream_in_editor", "enable"), &VoxelTerrain::set_run_stream_in_editor);
ClassDB::bind_method(D_METHOD("is_stream_running_in_editor"), &VoxelTerrain::is_stream_running_in_editor);
ClassDB::bind_method(
D_METHOD("set_automatic_loading_enabled", "enable"), &VoxelTerrain::set_automatic_loading_enabled);
ClassDB::bind_method(D_METHOD("is_automatic_loading_enabled"), &VoxelTerrain::is_automatic_loading_enabled);
// TODO Rename `_voxel_bounds`
ClassDB::bind_method(D_METHOD("set_bounds"), &VoxelTerrain::_b_set_bounds);
ClassDB::bind_method(D_METHOD("get_bounds"), &VoxelTerrain::_b_get_bounds);
//ClassDB::bind_method(D_METHOD("_on_stream_params_changed"), &VoxelTerrain::_on_stream_params_changed);
ClassDB::bind_method(D_METHOD("try_set_block_data", "position", "voxels", "replace_if_exists"),
&VoxelTerrain::_b_try_set_block_data);
ClassDB::bind_method(D_METHOD("get_viewer_network_peer_ids_in_area", "area_origin", "area_size"),
&VoxelTerrain::_b_get_viewer_network_peer_ids_in_area);
ClassDB::bind_method(D_METHOD("has_block", "block_position"), &VoxelTerrain::has_block);
GDVIRTUAL_BIND(_on_data_block_enter, "info");
GDVIRTUAL_BIND(_on_area_edited, "area_origin", "area_size");
ADD_GROUP("Bounds", "");
@ -1412,6 +1645,18 @@ void VoxelTerrain::_bind_methods() {
"get_collision_mask");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "collision_margin"), "set_collision_margin", "get_collision_margin");
ADD_GROUP("Networking", "");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "block_enter_notification_enabled"),
"set_block_enter_notification_enabled", "is_block_enter_notification_enabled");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "area_edit_notification_enabled"), "set_area_edit_notification_enabled",
"is_area_edit_notification_enabled");
// This may be set to false in multiplayer designs where the server is the one sending the blocks
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "automatic_loading_enabled"), "set_automatic_loading_enabled",
"is_automatic_loading_enabled");
ADD_GROUP("Advanced", "");
// TODO Should probably be in the parent class?

View File

@ -3,6 +3,8 @@
#include "../server/voxel_server.h"
#include "../storage/voxel_data_map.h"
#include "../util/godot/funcs.h"
#include "voxel_data_block_enter_info.h"
#include "voxel_mesh_map.h"
#include "voxel_node.h"
@ -64,9 +66,14 @@ public:
unsigned int get_max_view_distance() const;
void set_max_view_distance(unsigned int distance_in_voxels);
// TODO Make this obsolete with multi-viewers
void set_viewer_path(NodePath path);
NodePath get_viewer_path() const;
void set_block_enter_notification_enabled(bool enable);
bool is_block_enter_notification_enabled() const;
void set_area_edit_notification_enabled(bool enable);
bool is_area_edit_notification_enabled() const;
void set_automatic_loading_enabled(bool enable);
bool is_automatic_loading_enabled() const;
void set_material(unsigned int id, Ref<Material> material);
Ref<Material> get_material(unsigned int id) const;
@ -80,6 +87,13 @@ public:
Ref<VoxelTool> get_voxel_tool();
// Creates or overrides whatever block data there is at the given position.
// The use case is multiplayer, client-side.
// If no local viewer is actually in range, the data will not be applied and the function returns `false`.
bool try_set_block_data(Vector3i position, std::shared_ptr<VoxelBufferInternal> &voxel_data);
bool has_block(Vector3i position) const;
void set_run_stream_in_editor(bool enable);
bool is_stream_running_in_editor() const;
@ -89,6 +103,11 @@ public:
void restart_stream() override;
void remesh_all_blocks() override;
// Asks to generate (or re-generate) a block at the given position asynchronously.
// If the block already exists once the block is generated, it will be cancelled.
// If the block is out of range of any viewer, it will be cancelled.
void generate_block_async(Vector3i block_position);
// For convenience, this is actually stored in a particular type of mesher
Ref<VoxelBlockyLibrary> get_voxel_library() const;
@ -135,7 +154,7 @@ private:
void stop_streamer();
void reset_map();
void view_data_block(Vector3i bpos);
void view_data_block(Vector3i bpos, uint32_t viewer_id, bool require_notification);
void view_mesh_block(Vector3i bpos, bool mesh_flag, bool collision_flag);
void unview_data_block(Vector3i bpos);
void unview_mesh_block(Vector3i bpos, bool mesh_flag, bool collision_flag);
@ -154,6 +173,18 @@ private:
bool try_get_paired_viewer_index(uint32_t id, size_t &out_i) const;
void notify_data_block_enter(VoxelDataBlock &block, uint32_t viewer_id);
void get_viewers_in_area(std::vector<int> &out_viewer_ids, Box3i voxel_box) const;
// Called each time a data block enters a viewer's area.
// This can be either when the block exists and the viewer gets close enough, or when it gets loaded.
// This only happens if data block enter notifications are enabled.
GDVIRTUAL1(_on_data_block_enter, VoxelDataBlockEnterInfo *);
// Called each time voxels are edited within a region.
GDVIRTUAL2(_on_area_edited, Vector3i, Vector3i);
static void _bind_methods();
// Bindings
@ -164,14 +195,17 @@ private:
void _b_save_block(Vector3i p_block_pos);
void _b_set_bounds(AABB aabb);
AABB _b_get_bounds() const;
bool _b_try_set_block_data(Vector3i position, Ref<VoxelBuffer> voxel_data);
Dictionary _b_get_statistics() const;
PackedInt32Array _b_get_viewer_network_peer_ids_in_area(Vector3i area_origin, Vector3i area_size) const;
uint32_t _volume_id = 0;
// Paired viewers are VoxelViewers which intersect with the boundaries of the volume
struct PairedViewer {
struct State {
Vector3i local_position_voxels;
Box3i data_box;
Box3i data_box; // In block coordinates
Box3i mesh_box;
int view_distance_voxels = 0;
bool requires_collisions = false;
@ -201,12 +235,21 @@ private:
struct LoadingBlock {
VoxelRefCount viewers;
// TODO Optimize allocations here
std::vector<uint32_t> viewers_to_notify;
};
// Blocks currently being loaded.
HashMap<Vector3i, LoadingBlock, Vector3iHasher> _loading_blocks;
std::vector<Vector3i> _blocks_pending_load; // The order in that list does not matter
std::vector<Vector3i> _blocks_pending_update; // The order in that list does not matter
std::vector<BlockToSave> _blocks_to_save; // The order in that list does not matter
// Blocks that should be loaded on the next process call.
// The order in that list does not matter.
std::vector<Vector3i> _blocks_pending_load;
// Block meshes that should be updated on the next process call.
// The order in that list does not matter.
std::vector<Vector3i> _blocks_pending_update;
// Blocks that should be saved on the next process call.
// The order in that list does not matter.
std::vector<BlockToSave> _blocks_to_save;
Ref<VoxelStream> _stream;
Ref<VoxelMesher> _mesher;
@ -218,9 +261,15 @@ private:
float _collision_margin = constants::DEFAULT_COLLISION_MARGIN;
bool _run_stream_in_editor = true;
//bool _stream_enabled = false;
bool _block_enter_notification_enabled = false;
bool _area_edit_notification_enabled = false;
// If enabled, VoxelViewers will cause blocks to automatically load around them.
bool _automatic_loading_enabled = true;
Ref<Material> _materials[VoxelMesherBlocky::MAX_MATERIALS];
GodotUniqueObjectPtr<VoxelDataBlockEnterInfo> _data_block_enter_info_obj;
Stats _stats;
};

View File

@ -41,6 +41,28 @@ bool VoxelViewer::is_requiring_collisions() const {
return _requires_collisions;
}
void VoxelViewer::set_requires_data_block_notifications(bool enabled) {
_requires_data_block_notifications = enabled;
if (is_active()) {
VoxelServer::get_singleton()->set_viewer_requires_data_block_notifications(_viewer_id, enabled);
}
}
bool VoxelViewer::is_requiring_data_block_notifications() const {
return _requires_data_block_notifications;
}
void VoxelViewer::set_network_peer_id(int id) {
_network_peer_id = id;
if (is_active()) {
VoxelServer::get_singleton()->set_viewer_network_peer_id(_viewer_id, id);
}
}
int VoxelViewer::get_network_peer_id() const {
return _network_peer_id;
}
void VoxelViewer::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_TREE: {
@ -49,6 +71,9 @@ void VoxelViewer::_notification(int p_what) {
VoxelServer::get_singleton()->set_viewer_distance(_viewer_id, _view_distance);
VoxelServer::get_singleton()->set_viewer_requires_visuals(_viewer_id, _requires_visuals);
VoxelServer::get_singleton()->set_viewer_requires_collisions(_viewer_id, _requires_collisions);
VoxelServer::get_singleton()->set_viewer_requires_data_block_notifications(
_viewer_id, _requires_data_block_notifications);
VoxelServer::get_singleton()->set_viewer_network_peer_id(_viewer_id, _network_peer_id);
const Vector3 pos = get_global_transform().origin;
VoxelServer::get_singleton()->set_viewer_position(_viewer_id, pos);
}
@ -90,6 +115,8 @@ void VoxelViewer::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "requires_visuals"), "set_requires_visuals", "is_requiring_visuals");
ADD_PROPERTY(
PropertyInfo(Variant::BOOL, "requires_collisions"), "set_requires_collisions", "is_requiring_collisions");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "requires_data_block_notifications"),
"set_requires_data_block_notifications", "is_requiring_data_block_notifications");
}
} // namespace zylann::voxel

View File

@ -23,6 +23,12 @@ public:
void set_requires_collisions(bool enabled);
bool is_requiring_collisions() const;
void set_requires_data_block_notifications(bool enabled);
bool is_requiring_data_block_notifications() const;
void set_network_peer_id(int id);
int get_network_peer_id() const;
protected:
void _notification(int p_what);
@ -36,6 +42,8 @@ private:
unsigned int _view_distance = 128;
bool _requires_visuals = true;
bool _requires_collisions = true;
bool _requires_data_block_notifications = false;
int _network_peer_id = -1;
};
} // namespace zylann::voxel

View File

@ -62,9 +62,28 @@ inline std::shared_ptr<T> gd_make_shared(Arg0_T arg0, Arg1_T arg1, Arg2_T arg2)
return std::shared_ptr<T>(memnew(T(arg0, arg1, arg2)), memdelete<T>);
}
// For use with smart pointers such as std::unique_ptr
template <typename T>
struct GodotObjectDeleter {
void operator()(T *obj) {
memdelete(obj);
}
};
// Specialization of `std::unique_ptr which uses `memdelete()` as deleter.
template <typename T>
using GodotUniqueObjectPtr = std::unique_ptr<T, GodotObjectDeleter<T>>;
// Creates a `GodotUniqueObjectPtr<T>` with an object constructed with `memnew()` inside.
template <typename T>
GodotUniqueObjectPtr<T> gd_make_unique() {
return GodotUniqueObjectPtr<T>(memnew(T));
}
void set_nodes_owner(Node *root, Node *owner);
void set_nodes_owner_except_root(Node *root, Node *owner);
// To allow using Ref<T> as key in Godot's HashMap
template <typename T>
struct RefHasher {
static _FORCE_INLINE_ uint32_t hash(const Ref<T> &v) {