Skip to content

Commit

Permalink
Working network replication for ECS.
Browse files Browse the repository at this point in the history
  • Loading branch information
daid committed Oct 28, 2022
1 parent d3cebb2 commit 545491d
Show file tree
Hide file tree
Showing 10 changed files with 90 additions and 12 deletions.
2 changes: 2 additions & 0 deletions src/container/sparseset.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ template<typename T> class SparseSet final

Iterator begin() { return Iterator(*this, dense.size() - 1); }
Iterator end() { return Iterator(*this, std::numeric_limits<size_t>::max()); }

size_t size() { return data.size(); }
private:
std::vector<uint32_t> sparse;
std::vector<uint32_t> dense;
Expand Down
6 changes: 6 additions & 0 deletions src/ecs/component.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,10 @@ void ComponentStorageBase::destroyAll(uint32_t index)
storage->destroy(index);
}

void ComponentStorageBase::dumpDebugInfo()
{
for(auto storage = all_component_storage; storage; storage = storage->next)
storage->dumpDebugInfoImpl();
}

}
8 changes: 8 additions & 0 deletions src/ecs/component.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "container/sparseset.h"
#include "logging.h"

class GameServer;
template<typename T> class MultiplayerECSComponentReplication;
Expand All @@ -10,11 +11,13 @@ class ComponentStorageBase {
public:
ComponentStorageBase();

static void dumpDebugInfo();
private:
ComponentStorageBase* next = nullptr;
protected:
static void destroyAll(uint32_t index);
virtual void destroy(uint32_t index) = 0;
virtual void dumpDebugInfoImpl() = 0;

friend class Entity;
};
Expand All @@ -25,6 +28,11 @@ template<typename T> class ComponentStorage : public ComponentStorageBase {
sparseset.remove(index);
}

virtual void dumpDebugInfoImpl() override
{
LOG(Debug, "Component:", typeid(T).name(), " Count: ", storage.sparseset.size());
}

SparseSet<T> sparseset;

static inline ComponentStorage<T> storage;
Expand Down
8 changes: 7 additions & 1 deletion src/ecs/entity.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "entity.h"
#include "component.h"
#include "logging.h"
#include <vector>

namespace sp::ecs {
Expand Down Expand Up @@ -33,7 +34,7 @@ Entity Entity::fromIndex(uint32_t index)

Entity::operator bool() const
{
if (index == std::numeric_limits<uint32_t>::max())
if (index == no_index)
return false;
return entity_version[index] == version;
}
Expand Down Expand Up @@ -69,4 +70,9 @@ void Entity::destroy()
free_list.push_back(index);
}

void Entity::dumpDebugInfo()
{
LOG(Debug, "Entity count:", entity_version.size() - free_list.size(), " Free entities:", free_list.size());
}

}
10 changes: 7 additions & 3 deletions src/ecs/entity.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,15 @@ class Entity final {
bool operator!=(const Entity& other) const;

uint32_t getIndex() { return index; } // You should never need this, but the multiplayer code does need it.

static void dumpDebugInfo();

static constexpr uint32_t no_index = std::numeric_limits<uint32_t>::max();
private:
static constexpr uint32_t destroyed_flag = 1 << 31;
static constexpr uint32_t destroyed_flag = 1 << (std::numeric_limits<uint32_t>::digits - 1);

uint32_t index = std::numeric_limits<uint32_t>::max();
uint32_t version = std::numeric_limits<uint32_t>::max();
uint32_t index = no_index;
uint32_t version = no_index;

static Entity fromIndex(uint32_t index);
static std::vector<uint32_t> entity_version;
Expand Down
4 changes: 4 additions & 0 deletions src/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "windowManager.h"
#include "scriptInterface.h"
#include "multiplayer_server.h"
#include "ecs/entity.h"

#include <thread>
#include <SDL.h>
Expand Down Expand Up @@ -218,6 +219,9 @@ void Engine::handleEvent(SDL_Event& event)
}
printf("%4d %s\n",grand_total,"All PObjects");
printf("------------------------\n");

sp::ecs::Entity::dumpDebugInfo();
sp::ecs::ComponentStorageBase::dumpDebugInfo();
}
#endif

Expand Down
10 changes: 9 additions & 1 deletion src/multiplayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,20 @@ template <> void multiplayerReplicationFunctions<sp::ecs::Entity>::sendData(void
if (*e) {
packet << e->getIndex();
} else {
packet << std::numeric_limits<uint32_t>::max();
packet << sp::ecs::Entity::no_index;
}
}

template <> void multiplayerReplicationFunctions<sp::ecs::Entity>::receiveData(void* data, sp::io::DataBuffer& packet)
{
auto e = (sp::ecs::Entity*)data;

uint32_t index;
packet >> index;
if (index < game_client->entity_mapping.size())
*e = game_client->entity_mapping[index];
else
*e = sp::ecs::Entity{};
}

static bool collisionable_isChanged(void* data, void* prev_data_ptr)
Expand Down
25 changes: 23 additions & 2 deletions src/multiplayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -310,15 +310,35 @@ class MultiplayerECSComponentReplicationBase {
component_index = next->component_index + 1;
}

//Called on the server so reused component slots get properly send if an entity is destroyed and reused within the same tick.
// And that destroyed entities don't need to send "destroyed component" packets for each component.
virtual void onEntityDestroyed(uint32_t index) = 0;

virtual void sendAll(sp::io::DataBuffer& packet) = 0;
virtual void update(sp::io::DataBuffer& packet) = 0;
virtual void receive(sp::ecs::Entity entity, sp::io::DataBuffer& packet) = 0;
virtual void remove(sp::ecs::Entity entity) = 0;
virtual void receive(sp::ecs::Entity entity, sp::io::DataBuffer& packet) = 0; //Called from client
virtual void remove(sp::ecs::Entity entity) = 0; //Called from client
};

//Simple replication setup that just sends over the whole component each time it is changed.
template<typename T> class MultiplayerECSComponentReplication : public MultiplayerECSComponentReplicationBase
{
public:
sp::SparseSet<T> component_copy;

void onEntityDestroyed(uint32_t index) override
{
component_copy.remove(index);
}

void sendAll(sp::io::DataBuffer& packet) override
{
for(auto [index, data] : sp::ecs::ComponentStorage<T>::storage.sparseset)
{
packet << CMD_ECS_SET_COMPONENT << component_index << index << data;
}
}

void update(sp::io::DataBuffer& packet) override
{
for(auto [index, data] : sp::ecs::ComponentStorage<T>::storage.sparseset)
Expand All @@ -343,6 +363,7 @@ template<typename T> class MultiplayerECSComponentReplication : public Multiplay
packet >> data;
entity.addComponent<T>(data);
}

void remove(sp::ecs::Entity entity) override
{
entity.removeComponent<T>();
Expand Down
8 changes: 5 additions & 3 deletions src/multiplayer_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,10 @@ void GameClient::update(float /*delta*/)
uint32_t index;
packet >> component_index >> index;
for(auto ecsrb = MultiplayerECSComponentReplicationBase::first; ecsrb; ecsrb=ecsrb->next) {
if (ecsrb->component_index == component_index)
ecsrb->receive(entity_mapping[index], packet);
if (ecsrb->component_index == component_index) {
if (index < entity_mapping.size() && entity_mapping[index])
ecsrb->receive(entity_mapping[index], packet);
}
}
}
break;
Expand All @@ -288,7 +290,7 @@ void GameClient::update(float /*delta*/)
uint32_t index;
packet >> component_index >> index;
for(auto ecsrb = MultiplayerECSComponentReplicationBase::first; ecsrb; ecsrb=ecsrb->next) {
if (ecsrb->component_index == component_index)
if (ecsrb->component_index == component_index && index < entity_mapping.size())
ecsrb->remove(entity_mapping[index]);
}
}
Expand Down
21 changes: 19 additions & 2 deletions src/multiplayer_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,14 @@ void GameServer::update(float /*gameDelta*/)
ecs_packet << CMD_ECS_UPDATE;
// For each entity, check which version number we last transmitted and if it is changed, transmit creation/deletion of entities.
ecs_entity_version.resize(sp::ecs::Entity::entity_version.size(), std::numeric_limits<uint32_t>::max());
for(uint32_t index; index<sp::ecs::Entity::entity_version.size(); index++) {
for(uint32_t index=0; index<sp::ecs::Entity::entity_version.size(); index++) {
if (ecs_entity_version[index] != sp::ecs::Entity::entity_version[index]) {
if (ecs_entity_version[index] & sp::ecs::Entity::destroyed_flag)
if (!(ecs_entity_version[index] & sp::ecs::Entity::destroyed_flag)) {
ecs_packet << CMD_ECS_ENTITY_DESTROY << index;
for(auto ecsrb = MultiplayerECSComponentReplicationBase::first; ecsrb; ecsrb=ecsrb->next) {
ecsrb->onEntityDestroyed(index);
}
}
ecs_entity_version[index] = sp::ecs::Entity::entity_version[index];
if (!(ecs_entity_version[index] & sp::ecs::Entity::destroyed_flag))
ecs_packet << CMD_ECS_ENTITY_CREATE << index;
Expand Down Expand Up @@ -475,6 +479,19 @@ void GameServer::handleNewClient(ClientInfo& info)

onNewClient(info.client_id);

//Replicate ECS data, we send this as one big packet so ECS state is always consistent on the client.
sp::io::DataBuffer ecs_packet;
ecs_packet << CMD_ECS_UPDATE;
// For each entity, check which version number we last transmitted and if it is changed, transmit creation/deletion of entities.
for(uint32_t index=0; index<sp::ecs::Entity::entity_version.size(); index++) {
if (!(sp::ecs::Entity::entity_version[index] & sp::ecs::Entity::destroyed_flag))
ecs_packet << CMD_ECS_ENTITY_CREATE << index;
}
// For each component type, send all existing components.
for(auto ecsrb = MultiplayerECSComponentReplicationBase::first; ecsrb; ecsrb=ecsrb->next)
ecsrb->sendAll(ecs_packet);
info.socket->queue(ecs_packet);

//On a new client, first create all the already existing objects. And update all the values.
for(std::unordered_map<int32_t, P<MultiplayerObject> >::iterator i=objectMap.begin(); i != objectMap.end(); i++)
{
Expand Down

1 comment on commit 545491d

@daid-tinyci
Copy link

@daid-tinyci daid-tinyci bot commented on 545491d Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TinyCI build failure:

[/home/tinyci/builds/daid/SeriousProton/_build_native:make -j 2] returned [2]:


[ 15%] Built target spfreetype2

[ 16%] Built target glad

[ 29%] Built target box2d

[ 37%] Built target lua

[ 43%] Built target basisu-encoder

[ 83%] Built target opus

[ 83%] Building CXX object CMakeFiles/seriousproton_objects.dir/src/engine.cpp.o

[ 83%] Building CXX object CMakeFiles/seriousproton_objects.dir/src/multiplayer.cpp.o

[ 84%] Building CXX object CMakeFiles/seriousproton_objects.dir/src/multiplayer_client.cpp.o

[ 84%] Building CXX object CMakeFiles/seriousproton_objects.dir/src/multiplayer_server.cpp.o

[ 84%] Building CXX object CMakeFiles/seriousproton_objects.dir/src/networkRecorder.cpp.o

[ 85%] Building CXX object CMakeFiles/seriousproton_objects.dir/src/ecs/entity.cpp.o

In file included from /data/tinyci_builds/daid/SeriousProton/src/ecs/component.h:3,

                 from /data/tinyci_builds/daid/SeriousProton/src/ecs/entity.h:6,

                 from /data/tinyci_builds/daid/SeriousProton/src/ecs/entity.cpp:1:

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h:60:35: error: ‘size_t’ has not been declared

   60 |         Iterator(SparseSet& _set, size_t _dense_index) : set(_set), dense_index(_dense_index) {}

      |                                   ^~~~~~

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h:70:9: error: ‘size_t’ does not name a type

   70 |         size_t dense_index;

      |         ^~~~~~

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h:5:1: note: ‘size_t’ is defined in header ‘<cstddef>’; did you forget to ‘#include <cstddef>’?

    4 | #include <vector>

  +++ |+#include <cstddef>

    5 | #include <limits>

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h:76:5: error: ‘size_t’ does not name a type

   76 |     size_t size() { return data.size(); }

      |     ^~~~~~

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h:76:5: note: ‘size_t’ is defined in header ‘<cstddef>’; did you forget to ‘#include <cstddef>’?

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h: In constructor ‘sp::SparseSet<T>::Iterator::Iterator(sp::SparseSet<T>&, int)’:

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h:60:69: error: class ‘sp::SparseSet<T>::Iterator’ does not have any field named ‘dense_index’

   60 |         Iterator(SparseSet& _set, size_t _dense_index) : set(_set), dense_index(_dense_index) {}

      |                                                                     ^~~~~~~~~~~

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h: In member function ‘bool sp::SparseSet<T>::Iterator::operator!=(const sp::SparseSet<T>::Iterator&) const’:

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h:62:63: error: ‘dense_index’ was not declared in this scope

   62 |         bool operator!=(const Iterator& other) const { return dense_index != other.dense_index; }

      |                                                               ^~~~~~~~~~~

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h: In member function ‘void sp::SparseSet<T>::Iterator::operator++()’:

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h:63:29: error: ‘dense_index’ was not declared in this scope

   63 |         void operator++() { dense_index--; }

      |                             ^~~~~~~~~~~

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h: In member function ‘std::pair<unsigned int, T&> sp::SparseSet<T>::Iterator::operator*()’:

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h:64:65: error: ‘dense_index’ was not declared in this scope

   64 |         std::pair<uint32_t, T&> operator*() { return {set.dense[dense_index], set.data[dense_index]}; }

      |                                                                 ^~~~~~~~~~~

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h: In member function ‘std::pair<unsigned int, const T&> sp::SparseSet<T>::Iterator::operator*() const’:

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h:65:77: error: ‘dense_index’ was not declared in this scope

   65 |         std::pair<uint32_t, const T&> operator*() const { return {set.dense[dense_index], set.data[dense_index]}; }

      |                                                                             ^~~~~~~~~~~

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h: In member function ‘bool sp::SparseSet<T>::Iterator::atEnd()’:

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h:67:31: error: ‘dense_index’ was not declared in this scope

   67 |         bool atEnd() { return dense_index == std::numeric_limits<size_t>::max(); }

      |                               ^~~~~~~~~~~

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h:67:66: error: ‘size_t’ was not declared in this scope; did you mean ‘std::size_t’?

   67 |         bool atEnd() { return dense_index == std::numeric_limits<size_t>::max(); }

      |                                                                  ^~~~~~

      |                                                                  std::size_t

In file included from /usr/include/c++/12/limits:42,

                 from /data/tinyci_builds/daid/SeriousProton/src/ecs/entity.h:4:

/usr/include/x86_64-linux-gnu/c++/12/bits/c++config.h:298:33: note: ‘std::size_t’ declared here

  298 |   typedef __SIZE_TYPE__         size_t;

      |                                 ^~~~~~

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h:67:72: error: template argument 1 is invalid

   67 |         bool atEnd() { return dense_index == std::numeric_limits<size_t>::max(); }

      |                                                                        ^

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h: In member function ‘sp::SparseSet<T>::Iterator sp::SparseSet<T>::end()’:

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h:74:65: error: ‘size_t’ was not declared in this scope; did you mean ‘std::size_t’?

   74 |     Iterator end() { return Iterator(*this, std::numeric_limits<size_t>::max()); }

      |                                                                 ^~~~~~

      |                                                                 std::size_t

/usr/include/x86_64-linux-gnu/c++/12/bits/c++config.h:298:33: note: ‘std::size_t’ declared here

  298 |   typedef __SIZE_TYPE__         size_t;

      |                                 ^~~~~~

/data/tinyci_builds/daid/SeriousProton/src/container/sparseset.h:74:71: error: template argument 1 is invalid

   74 |     Iterator end() { return Iterator(*this, std::numeric_limits<size_t>::max()); }

      |                                                                       ^

make[2]: *** [CMakeFiles/seriousproton_objects.dir/build.make:866: CMakeFiles/seriousproton_objects.dir/src/ecs/entity.cpp.o] Error 1

make[2]: *** Waiting for unfinished jobs....

make[1]: *** [CMakeFiles/Makefile2:238: CMakeFiles/seriousproton_objects.dir/all] Error 2

make: *** [Makefile:136: all] Error 2

Please sign in to comment.