I’ll admit I’ve been slacking quite a bit here since I’ve become horribly addicted to Valve’s new MOBA deadlock. It has quite possibly one of the best combat systems I’ve played, and definitely ever for a MOBA.
That being said, I’m still chugging away on my game. I try to at least open the editor everyday, even if it’s just renaming a variable or something! I’ve made a bit of progress on handling respawns so that will be the topic of todays discussion. But first, let’s talk about Damage.
Damage
Right now I’m only concerning myself with a character being able to only mouse left button to execute a melee attack. As discussed in the previous post in handling combat, I spawn a new entity in the world for instigating combat. On collision, we create a new entity called <combat::ApplyDamage>. This entity captures the instigator, target, where in world space the hit occurred, and the attack details (for now just base damage).
// Create DamageApplied entity so our system can pick it up and
// do the calculations necessary to apply damage
auto DamageApplied = Target.world().entity();
DamageApplied.set<ApplyDamage>({Details->Instigator, Target, InManifold.GetWorldSpaceContactPointOn1(0), Details->BaseDamage});
Our Damage system looks for any entities in the world that are ApplyDamage and execute the following flecs system:
GameWorld.system<ApplyDamage>("ApplyDamage")
.kind(flecs::PostUpdate)
.write<units::Attributes>()
.each([this](flecs::iter& It, size_t Index, ApplyDamage &Damage)
{
// ... check liveliness...//
// ...
if (Damage.TargetHit.has<combat::IsDead>())
{
Logger->Info("player already dying or dead.");
return;
}
auto RemainingDamage = Damage.BaseDamage;
auto EntityAttribs = Damage.TargetHit.get_mut<units::Attributes>();
auto RemainingShield = EntityAttribs->Shield - RemainingDamage;
EntityAttribs->Shield = glm_clamp(RemainingShield, 0, MaxShield);
// Set damage to whatever negative amount of shield is left or zero
RemainingDamage = (RemainingShield <= 0.f) ? abs(RemainingShield) : 0.f;
EntityAttribs->Health = glm_clamp(EntityAttribs->Health - RemainingDamage, 0, MaxHealth);
if (EntityAttribs->Health == 0.f)
{
Damage.TargetHit.set<combat::IsDead>({.DiedAt=0.f});
}
});
As you can see it’s not as straight forward as subtract hit from player health, we have to worry about shields (and probably other systems will come into play here). What we do is we look up the players shield and first subtract that (being sure to clamp their actual shield value to zero so it doesn’t go negative). Our temporary RemainingShield however, was not clamped, so any negative values we have left over will then considered RemainingDamage to be applied to their health. We then take the players health and subtract the remaining damage, again being sure to clamp to zero so we don’t go negative.
Death
If the health goes zero, we apply the combat::IsDead component (tag) to the player / or entity. We can now use this tag to disable a bunch of other systems using flecs’s without specifier! So for example, we don’t want to read their input any more so we added the following specification to our Input handling flecs system:
GameWorld.observer<network::Inputs>("OnNewInputs")
.event(flecs::OnSet)
.without<combat::IsDead>()
.write<network::InputsHistory>()
.each([=](flecs::iter &It, size_t Index, network::Inputs &Inputs)
{
// system goes here //
});
The above system will not be called on entities that have the combat::IsDead tag even if they have the network::Inputs component set with a new value! (flecs has something for everything I swear).
Respawning
Here comes the tricky part, how to respawn the player on both the server & client. This smells like an RPC problem. As shown above, here’s the entire breakdown of the respawn logic (at least for the happy path):

Right now the UI/Menu system for respawning has not been implemented, so our flec’s RequestRespawn system assumes this tag will be added by the UI when clicking respawn:
// Only send respawn request if the user is dead and requested respawn
// Remove combat::RequestRespawn
GameWorld.system<network::User, network::PMOServer, network::Connected, combat::IsDead, combat::RequestRespawn>("RequestRespawn")
.each([&](flecs::iter& It, size_t Index, network::User &Network, network::PMOServer &Server, network::Connected, combat::IsDead, combat::RequestRespawn &Request)
{
// Already requested a respawn, exit early
if (Request.RequestedTime > 0.f)
{
Request.RequestedTime += It.delta_time();
return;
}
flatbuffers::FlatBufferBuilder Builder(128);
ClientCommandBuilder CmdBuilder(Builder);
CmdBuilder.add_opcode(Game::Message::ClientOpCode_RequestRespawn);
CmdBuilder.add_server_seq_id_ack(0);
Builder.Finish(CmdBuilder.Finish());
auto OutputLen = Builder.GetSize();
auto ClientCommands = std::make_unique<std::vector<uint8_t>>();
ClientCommands->reserve(OutputLen);
std::memcpy(ClientCommands->data(), Builder.GetBufferPointer(), Builder.GetSize());
Builder.Release();
auto RespawnRequest = std::make_unique<GameMessage>(Network.UserId, std::move(ClientCommands), Game::Message::InternalMessage::Socket);
RespawnRequest->MessageReliability = Game::Message::Reliability::Reliable;
RespawnRequest->OpCode = Game::Message::ClientOpCode_RequestRespawn;
Server.OutQueue->Push(std::move(RespawnRequest));
Request.RequestedTime = 0.1f; // initialize it
});
Rather straight forward, we build a flatbuffer message saying the user is requesting respawn. The only difference to our usual flatbuffer serialization is, we are setting our MessageReliability to Reliable. This is the first time we’ve had to do this. All world state updates and inputs are Unreliable, as we don’t care if they get lost as we’ll resend the delta. This time however, we need to get a response from the server, hence we set reliability. This is a separate channel in our ReliablePeer and every tick of the game and client will process any reliable messages that haven’t been ack’d by the remote endpoint.
If you want a refresher on how all that works, see my UDP reliability series. One problem I forgot to handle though is, what to do if a reliable message fails? We need to handle these failure cases or at least be notified if the respawn message from the server never gets back to us. For that I once again use the lock free queue as an IPC mechanism between the socket/reliability layer and the game loop.
After some slight modifications, we pass in an OpCode for the reliable message we sent: RespawnRequest->OpCode = Game::Message::ClientOpCode_RequestRespawn. Now the reliability layer can respond saying that OpCode failed to get processed when it times out / is removed. An alternative would be to attach a callback to each message and process that callback on failure. I may move to this system in the future if necessary. But for now here is what I’m doing:
// Delete this message if we are above the resend threshold.
if (TimeDiff > DefaultConfig.ResendThreshold)
{
NotifyMessageFailed(*Message, InboundQueue);
Endpoint.GetSentPackets()->RemoveWithCleanup(Message->Sequence);
continue;
}
// ... //
void ReliablePeer::NotifyMessageFailed(net::ReliableMessage &Message, LockFreeQueue<std::unique_ptr<Game::Message::GameMessage>, 1000000> &InboundQueue) const
{
InboundQueue.Push(std::make_unique<GameMessage>(UserId, ReliableMsgFailed, Message.OpCode));
}
Then in our client or server game loop we can just pull that InternalMessage off the queue and process it:
case Game::Message::InternalMessage::ReliableMsgFailed:
{
PMOWorld->ProcessMessageFailed(static_cast<Game::Message::ClientOpCode>(GameData->get()->OpCode));
break;
}
// ...
void PMOWorld::ProcessMessageFailed(const Game::Message::ClientOpCode OpCode)
{
switch (OpCode)
{
case Game::Message::ClientOpCode_RequestRespawn:
{
if (Player.has<combat::RequestRespawn>())
{
Log->Error("failed to respawn player");
}
else
{
Log->Error("got failed request respawn message but player doesn't have combat::RequestRespawn");
}
break;
}
}
}
Now, assuming we do get the message processed by the server it receives the message in it’s client processing code in the main game loop (after being forwarded over the lock free queue IPC mechanism):
switch (Command->opcode())
{
case Game::Message::ClientOpCode::ClientOpCode_Input:
{
ServerProcessClientInput(PlayerEntity, UserId, SequenceId, Command);
break;
}
case Game::Message::ClientOpCode::ClientOpCode_RequestRespawn:
{
if (!PlayerEntity.has<combat::IsDead>())
{
Logger->Warn("Player {} requested respawn but isn't dead.", UserId);
return;
}
// Tell the ECS system we are respawning
PlayerEntity.add<character::Respawn>(); // See RespawnPlayer system
auto BindLocation = PlayerEntity.get<character::BindLocation>();
flatbuffers::FlatBufferBuilder Builder(128);
ServerCommandBuilder CmdBuilder(Builder);
CmdBuilder.add_opcode(Game::Message::ServerOpCode_Respawn);
auto RespawnLocation = Game::Message::Vec3{BindLocation->Location.vec3[0], BindLocation->Location.vec3[1], BindLocation->Location.vec3[2]};
CmdBuilder.add_respawn_location(&RespawnLocation);
Builder.Finish(CmdBuilder.Finish());
auto OutputLen = Builder.GetSize();
auto ServerData = std::make_unique<std::vector<uint8_t>>();
ServerData->reserve(OutputLen);
std::memcpy(ServerData->data(), Builder.GetBufferPointer(), Builder.GetSize());
Builder.Release();
auto RespawnReply = std::make_unique<GameMessage>(UserId, std::move(ServerData), Game::Message::InternalMessage::Socket);
OutQueue->Push(std::move(RespawnReply));
break;
}
default:
{
break;
}
}
This processing code adds the character::Respawn tag which is then picked up on the next tick by our RespawnPlayer flecs system:
// Respawn player in the world
GameWorld.system<character::Respawn, network::User, network::Connected, character::PMOCharacter, character::BindLocation>("RespawnPlayer")
.kind(flecs::PreStore)
.each([&](flecs::iter& It, size_t Index, character::Respawn, network::User &User, network::Connected, character::PMOCharacter &Character, character::BindLocation &BindLocation)
{
auto RespawnedPlayer = It.entity(Index);
RespawnedPlayer.set<units::Vector3>({BindLocation.Location})
.set<units::Quat>({})
.set<units::Velocity>({})
.set<units::Attributes>({});
Character.SetLocation(BindLocation.Location);
RespawnedPlayer.remove<character::Respawn, combat::IsDead>();
});
And that, is how we deal with respawns! I will admit this is probably not the cleanest of architectures. I may have to redesign all of this message passing sometime in the future, but for now the amount of RPC calls are pretty limited since our state / input updates handle most of the current functionality.
Time will tell if this design can scale out to handle other game events such as picking up items, dropping items, placing structures and so forth.
