The cleanest way to de-couple these kinds of things is to design the entity system such that each property is observable, as well as the entity set itself being observable.Each entity has some type and can be programmatically created; each entity is introspectable so that all properties can be extracted.This system can also be used for save-game and level editors. btw.

Additionally, events should similarly be introspectable and observable. Any destination for an event should use some kind of identity that's not a pointer. And, in general, sub-components of entities and of systems should generally be identified by ID, not pointer, to make talking about them on the network easier.

Then, the network system can install itself as an observer on the entity set. When an entity is created, it observes this, and introspects the entity to extract its type and properties, and send an appropriate "create this entity" message to all listeners.You can also implement a route for events. This means that if the client wants to create an entity, the "create an entity" endpoint object in the client would route that message to the server, rather than do it locally.

Additionally, the "set" operation for properties may need to support injection of behavior. "Set" for "Position" on a locally mirrored entity might want to do over-time interpolation, rather than straight-jump, for example. A sufficient amount of template metaprogramming and smart defaults for "client" versus "server" behaviors can make this simple to express for the entity creation, but the underlying systems need pretty careful attention to detail and implementation for it to work out.

You then need to express your entire game logic in terms of these introspectable, observable, properties and types. That's a lot of re-factoring if you already have a game that uses more traditional "big struct of stuff" approaches. Although the observable interface can be expressed on top, by having each object register observable properties using pointer-to-date-storage.

when you have all of this set-up, you can choose whether to use networking or not (and what kind of networking) by selecting what you observe and what you inject.

For what it's worth, most games don't actually do this. They instead use the "Blob" design pattern (also known as "The Big Ball of Mud") where each entity knows about the network, and Does What It Takes  to make it all work. And, honestly, most games have a simple enough object model that I don't blame them; that may very well be the right choice for many such cases.

This leads to a few questions:Obviously, the network system needs access to a lot of information, how do people usually decouple this?

Networking is not easy to decouple. It's often not fully practical. The low-level implementation should be a separate module, of course, but actually networking a game requires conscious and deliberate decisions in the implementation of game logic, the game design, and even art. There is no way to magically make a game networked in a completely decoupled and transparent fashion.

Should events be send somehow to the network system, or should the system poll for data and events itself?

We implemented a polling system for a rough draft system recently (had to get a non-networked big game engine networked in a few weeks for proof-of-concept reasons) and the performance is _terrible_. Imagine that you have 1,000 network-aware game objects in your world. You have to poll every object every network tick, and polling the object typically means iterating over the networked properties and comparing their values with old values to see if a change happened, which likewise is very hard to do in a type-agnostic decoupled way.

An explicit system works much better. If a networked property on a component changes, the setter code in the component (or owning system if using an ECS) should signal to the network layer precisely which property on which component on which game object changed, and what the change was (e.g. pass in old and new values so delta compression is possible).

Different component properties may need to be handled very differently, too. Some properties are basically "changes almost every tick" like position/rotation and are streaming non-precious data (if you lose a change, it doesn't matter, because next tick you'll get another update anyway) while other properties are precious (the remote end really needs to know the exact current value, but if the value flips from A to B and back to A again it doesn't matter if the value B was lost) while yet other properties are _transition sensitive_ (the remote end needs to know that the property flipped from A to B and back and cannot lose that transition). This again is why you can't fully decouple networking; a separate networking engine can't possibly know which properties on which components have which requirements without the component informing the network engine.

Messages have to be set up in such a way that you're not sending every single game message over the wire but only the ones that need to be replicated to remote ends. Again, a generic decoupled networking engine can't know this by itself.

There's then the security issues. You can't just read any update over the wire and apply it since then a client could just send health updates every tick constantly giving itself 1000 HP and becoming effectively invincible. The networking definition needs to be very clear on which properties are server-authoritative and which are client-authoritative (there should be few to none of the latter in most games). Likewise for messages, you need to be clear about which end is allowed to send or receive which messages, so clients can't send state-change messages to the server for things the server should be authoritative over.

Ultimately you need to build a network engine that decouples the details of how state changes are sent/received, how messages are sent/received, how decoding works, the network tick, low-level TCP/UDP/IP details, etc. but which exposes high-level operations and meta-data configuration logic to the component/system and game logic code. That code then has to use those high-level operations, working with design and art to ensure that the game plays smoothly, fairly, and hides the inherent network latency and lag spikes. Decouple the implementation but be prepared to spend considerable time integrating networking support at an intimate level with much of the rest of the game's code.