How We Successfully Implemented the Weapon and Projectile System in Cockfite

From the start, our goal for Cockfite was clear: to build a responsive and precisely synchronized multiplayer weapon system that still feels dynamic and fluid. In this blog post, we want to share how we achieved that system technically, and what challenges we had to overcome along the way.

Core Architecture

Our weapon system is based on an abstract Weapon class, which handles general functionality like ammo, positioning, visuals, and integration with the inventory system.

Specific weapon types such as FirearmWeapon and ThrowableWeapon inherit from this base class. These can implement their own Fire() logic and shoot different types of projectiles.

Integrating Projectile Data

To build a synchronized projectile system, we use a generic networked buffer system based on Fusion’s INetworkStruct:

[StructLayout(LayoutKind.Explicit)]
public struct TrajectoryData : INetworkStruct
{
    [FieldOffset(0)] public Vector3Compressed FirePosition;
    [FieldOffset(12)] public Vector3Compressed CurrentPosition;
    [FieldOffset(24)] public Vector3Compressed TargetPosition;
    [FieldOffset(36)] public int FireTick;
    [FieldOffset(40)] public byte PrefabIndex;
    [FieldOffset(41)] private byte _state;
    [FieldOffset(42)] private short _padding;

    public bool IsFinished { get => _state.IsBitSet(0); set => _state.SetBit(0, value); }
    public bool HasStopped { get => _state.IsBitSet(1); set => _state.SetBit(1, value); }
}

The projectile logic is processed in the TrajectoryProjectileBuffer, which stores and updates network data and syncs it with the visual representation in the game.

The Render Process

Projectiles are visualized during the Render() callback, interpolating between predicted and server-confirmed states. We distinguish between LocalRenderTime for local input and RemoteRenderTime for other players.

Incorrect predictions are rolled back automatically, as the server’s authoritative data overrides the local buffer. Once a projectile is marked as IsFinished, it is removed from the ring buffer and returned to the object pool.

The Biggest Pitfall: Wrong Buffer Reference

One of the most persistent bugs we faced was that projectiles were never confirmed on the client. The root cause was simple yet sneaky: we were referencing the TrajectoryProjectileBuffer of the host instead of the local player. This led to a mismatch in context and prevented server validation.

After switching to GetComponentsInParent<IProjectileBuffer>() and dynamically selecting the correct buffer, everything worked as expected.

Conclusion

The success of this system lies in the clear separation between data, logic, and rendering, as well as a solid networking architecture. Thanks to the buffer system and the use of object pooling for projectiles, we achieved a clean mix of performance, synchronization, and modularity.

We hope this technical insight was helpful. In our next post, we’ll dive deeper into the object pooling system and hit feedback. Stay tuned!

Schreibe einen Kommentar