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.
This commit is contained in:
parent
1631e186b5
commit
d1250ef0ad
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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>();
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
40
terrain/voxel_data_block_enter_info.cpp
Normal file
40
terrain/voxel_data_block_enter_info.cpp
Normal 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
|
33
terrain/voxel_data_block_enter_info.h
Normal file
33
terrain/voxel_data_block_enter_info.h
Normal 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
|
@ -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) {
|
||||
|
@ -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:
|
||||
|
@ -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 = █
|
||||
|
||||
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?
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user