Networking
In Minecraft wird Networking verwendet, damit Client und Server miteinander kommunizieren können. Networking ist ein weites Feld, daher ist diese Seite in mehrere Kategorien unterteilt.
Warum ist Networking wichtig?
Pakete sind das Kernkonzept von Minecraft Networking. Pakete bestehen aus beliebigen Daten, die entweder vom Server zum Client oder vom Client zum Server gesendet werden können. Das folgende Diagramm zeigt eine visuelle Darstellung der Networkingarchitektur in Fabric:

Beachte, dass die Pakete die Brücke zwischen dem Server und dem Client bilden. Das liegt daran, dass fast alles, was du im Spiel tust, in irgendeiner Weise mit Networking zu tun hat, ob du es weißt oder nicht. Wenn du zum Beispiel eine Chat-Nachricht sendest, wird ein Paket mit dem Inhalt an den Server geschickt. Der Server sendet dann ein weiteres Paket mit deiner Nachricht an alle anderen Clients.
Wichtig ist, dass immer ein Server läuft, auch im Einzelspielermodus und im LAN. Pakete werden immer noch zur Kommunikation zwischen dem Client und dem Server verwendet, auch wenn niemand anderes mit dir spielt. Wenn von Seiten bei Networking die Rede ist, werden die Begriffe "logischer Client" und "logischer Server" verwendet. Der integrierte Singleplayer/LAN-Server und der dedizierte Server sind beide logische Server, aber nur der dedizierte Server kann als physischer Server betrachtet werden.
Wenn der Status zwischen dem Client und dem Server nicht synchronisiert wird, kann es zu Problemen kommen, wenn der Server oder andere Clients nicht mit dem übereinstimmen, was ein anderer macht. Dies wird oft als "Desync" bezeichnet. Wenn du deinen eigenen Mod schreibst, musst du eventuell ein Datenpaket senden, um den Status des Servers und aller Clients synchron zu halten.
Eine Einführung in Networking
Einen Payload definieren
INFO
Eine Payload sind die Daten, die innerhalb eines Pakets gesendet werden.
Dies kann durch das Erstellen eines Java Record mit einem BlockPos-Parameter, der CustomPacketPayload implementiert, gelöst werden.
java
public record ClientboundSummonLightningPayload(BlockPos pos) implements CustomPacketPayload {
public static final Identifier SUMMON_LIGHTNING_PAYLOAD_ID = Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "summon_lightning");
public static final CustomPacketPayload.Type<ClientboundSummonLightningPayload> TYPE = new CustomPacketPayload.Type<>(SUMMON_LIGHTNING_PAYLOAD_ID);
public static final StreamCodec<RegistryFriendlyByteBuf, ClientboundSummonLightningPayload> CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, ClientboundSummonLightningPayload::pos, ClientboundSummonLightningPayload::new);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Zugleich haben wir folgendes definiert:
- Einen
Identifier, der zur Identifizierung des Payload unseres Pakets verwendet wird. In diesem Beispiel lautet der Bezeichnerexample-mod:summon_lightning.
java
public static final Identifier SUMMON_LIGHTNING_PAYLOAD_ID = Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "summon_lightning");1
- Eine öffentliche, statische Instanz von
CustomPayload.Typezur eindeutigen Identifizierung dieses benutzdefinierten Payloads. Wir werden auf diese ID sowohl in unserem allgemeinen als auch in unserem Client-Code verweisen.
java
public static final CustomPacketPayload.Type<ClientboundSummonLightningPayload> TYPE = new CustomPacketPayload.Type<>(SUMMON_LIGHTNING_PAYLOAD_ID);1
- Eine öffentliche, statische Instanz eines
StreamCodec, damit das Spiel weiß, wie es den Inhalt des Pakets serialisieren/deserialisieren kann.
java
public static final StreamCodec<RegistryFriendlyByteBuf, ClientboundSummonLightningPayload> CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, ClientboundSummonLightningPayload::pos, ClientboundSummonLightningPayload::new);1
Wir haben auch type überschrieben, um unsere Payload-ID zurückzugeben.
java
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}1
2
3
4
2
3
4
Einen Payload registrieren
Bevor wir ein Paket mit unserer benutzerdefinierten Nutzlast senden, müssen wir es auf beiden physischen Seiten registrieren.
Dies kann in unserem gemeinsamen Initialisierers mit Hilfe von PayloadTypeRegistry.clientboundPlay().register erfolgen, das einen CustomPayload.Type und einen StreamCodec entgegennimmt.
java
PayloadTypeRegistry.clientboundPlay().register(ClientboundSummonLightningPayload.TYPE, ClientboundSummonLightningPayload.CODEC);1
Eine ähnliche Methode existiert, um Client-to-Server Payloads zu registrieren: PayloadTypeRegistry.serverboundPlay().register.
Senden eines Pakets an den Client
Um ein Paket mit unserem benutzerdefinierten Payload zu senden, können wir ServerPlayNetworking.send verwenden, das einen ServerPlayer und einen CustomPayload entgegennimmt.
Beginnen wir mit der Erstellung unseres Blitz-Kartoffel-Item. Du kannst use überschreiben, um eine Aktion auszulösen, wenn das Item verwendet wird. In diesem Fall, lasst uns Pakete an die Spieler in den Serverlevel senden.
java
public class LightningTaterItem extends Item {
public LightningTaterItem(Properties properties) {
super(properties);
}
@Override
public InteractionResult use(Level level, Player user, InteractionHand hand) {
if (level.isClientSide()) {
return InteractionResult.PASS;
}
ClientboundSummonLightningPayload payload = new ClientboundSummonLightningPayload(user.blockPosition());
for (ServerPlayer player : PlayerLookup.level((ServerLevel) level)) {
ServerPlayNetworking.send(player, payload);
}
return InteractionResult.SUCCESS;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Schauen wir uns den obigen Code an.
Wir senden nur Pakete, wenn die Aktion auf dem Server initiiert wird, indem wir frühzeitig mit einer isClientSide()-Prüfung zurückkehren:
java
if (level.isClientSide()) {
return InteractionResult.PASS;
}1
2
3
2
3
Wir erstellen eine Instanz des Payloads mit der Position des Users:
java
ClientboundSummonLightningPayload payload = new ClientboundSummonLightningPayload(user.blockPosition());1
Schließlich rufen wir die Spieler in dem Serverlevel durch PlayerLookup ab und senden ein Paket an jeden Spieler.
java
for (ServerPlayer player : PlayerLookup.level((ServerLevel) level)) {
ServerPlayNetworking.send(player, payload);
}1
2
3
2
3
INFO
Fabric API bietet PlayerLookup, eine Sammlung von Hilfsfunktionen, die Spieler auf einem Server suchen.
Ein häufig verwendeter Begriff zur Beschreibung der Funktionalität dieser Methoden ist "Tracking". Es bedeutet, dass eine Entität oder ein Chunk auf dem Server dem Client eines Spielers bekannt ist (innerhalb seiner Sichtweite) und die Entität oder die Block Entität sollte die Clients über Änderungen informieren.
Tracking ist ein wichtiges Konzept für effizientes Networking, damit nur die notwendigen Spieler durch das Senden von Paketen über Änderungen informiert werden.
Empfangen eines Pakets auf dem Client
Um ein von einem Server gesendetes Paket auf dem Client zu empfangen, musst du angeben, wie du das eingehende Paket behandeln willst.
Dies kann im Client-Initialisierer erfolgen, indem ClientPlayNetworking.registerGlobalReceiver aufgerufen und ein CustomPayload.Type und ein PlayPayloadHandler übergeben wird, der ein funktionales Interface ist.
In diesem Fall definieren wir die auszulösende Aktion innerhalb der Implementierung der PlayPayloadHandler-Implementierung (als Lambda-Ausdruck).
java
ClientPlayNetworking.registerGlobalReceiver(ClientboundSummonLightningPayload.TYPE, (payload, context) -> {
ClientLevel level = context.client().level;
if (level == null) {
return;
}
BlockPos lightningPos = payload.pos();
LightningBolt entity = EntityType.LIGHTNING_BOLT.create(level, EntitySpawnReason.TRIGGERED);
if (entity != null) {
entity.setPos(lightningPos.getX(), lightningPos.getY(), lightningPos.getZ());
level.addEntity(entity);
}
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Schauen wir uns den obigen Code an.
Wir können auf die Daten aus unserem Payload zugreifen, indem wir die Getter-Methoden des Records aufrufen. In diesem Fall payload.pos(). Welche genutzt werden können, um die x,- y- und z-Positionen abzurufen.
java
BlockPos lightningPos = payload.pos();1
Schließlich erstellen wir eine LightningBolt und fügen diesen dem Level hinzu.
java
LightningBolt entity = EntityType.LIGHTNING_BOLT.create(level, EntitySpawnReason.TRIGGERED);
if (entity != null) {
entity.setPos(lightningPos.getX(), lightningPos.getY(), lightningPos.getZ());
level.addEntity(entity);
}1
2
3
4
5
6
2
3
4
5
6
Wenn du nun diesen Mod auf einem Server installierst und ein Spieler dein Item "Lightning Tater" benutzt, sehen alle Spieler Blitze, die an der Position des Users einschlagen.
Senden eines Pakets an den Server
Genau wie beim Senden eines Pakets an den Client erstellen wir zunächst einen benutzerdefinierten Payload. Dieses Mal, wenn ein Spieler eine giftige Kartoffel auf einer lebenden Entität verwendet, fordern wir den Server auf, den Leuchten-Effekt auf diese anzuwenden.
java
public record GiveGlowingEffectServerboundPayload(int entityId) implements CustomPacketPayload {
public static final Identifier GIVE_GLOWING_EFFECT_PAYLOAD_ID = Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "give_glowing_effect");
public static final CustomPacketPayload.Type<GiveGlowingEffectServerboundPayload> TYPE = new CustomPacketPayload.Type<>(GIVE_GLOWING_EFFECT_PAYLOAD_ID);
public static final StreamCodec<RegistryFriendlyByteBuf, GiveGlowingEffectServerboundPayload> CODEC = StreamCodec.composite(ByteBufCodecs.INT, GiveGlowingEffectServerboundPayload::entityId, GiveGlowingEffectServerboundPayload::new);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Wir übergeben den entsprechenden Codec zusammen mit einem Methodenverweis, um den Wert aus dem Record zu erhalten und diesen Codec zu bauen.
Dann registrieren wir unseren Payload in unserem gemeinsamen Initialisierer. Diesmal jedoch als Client-to-Server Payload unter Verwendung von PayloadTypeRegistry.serverboundPlay().register.
java
PayloadTypeRegistry.serverboundPlay().register(GiveGlowingEffectServerboundPayload.TYPE, GiveGlowingEffectServerboundPayload.CODEC);1
Um ein Paket zu senden, fügen wir eine Aktion hinzu, wenn der Spieler eine giftige Kartoffel benutzt. Wir werden das Event UseEntityCallback verwenden, um die Dinge übersichtlich zu halten.
Wir registrieren das Event in unserem Client-Initialisierer, und wir verwenden isClientSide(), um sicherzustellen, dass die Aktion nur auf dem logischen Client ausgelöst wird.
java
UseEntityCallback.EVENT.register((player, level, hand, entity, hitResult) -> {
if (!level.isClientSide()) {
return InteractionResult.PASS;
}
ItemStack usedItemStack = player.getItemInHand(hand);
if (entity instanceof LivingEntity && usedItemStack.is(Items.POISONOUS_POTATO) && hand == InteractionHand.MAIN_HAND) {
GiveGlowingEffectServerboundPayload payload = new GiveGlowingEffectServerboundPayload(hitResult.getEntity().getId());
ClientPlayNetworking.send(payload);
return InteractionResult.SUCCESS;
}
return InteractionResult.PASS;
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Wir erstellen eine Instanz unseres GiveGlowingEffectServerboundPayload mit den notwendigen Argumenten. In diesem Fall, der Netzwerk ID der Zielentität.
java
GiveGlowingEffectServerboundPayload payload = new GiveGlowingEffectServerboundPayload(hitResult.getEntity().getId());1
Schließlich senden wir ein Paket an den Server, indem wir ClientPlayNetworking.send mit der Instanz unseres GiveGlowingEffectServerboundPayload aufrufen.
java
ClientPlayNetworking.send(payload);1
Empfangen eines Pakets auf dem Server
Dies kann im gemeinsamen Initialisierer geschehen, indem man ServerPlayNetworking.registerGlobalReceiver aufruft und einen CustomPayload.Type und einen PlayPayloadHandler übergibt.
java
ServerPlayNetworking.registerGlobalReceiver(GiveGlowingEffectServerboundPayload.TYPE, (payload, context) -> {
Entity entity = context.player().level().getEntity(payload.entityId());
if (entity instanceof LivingEntity livingEntity && livingEntity.closerThan(context.player(), 5)) {
livingEntity.addEffect(new MobEffectInstance(MobEffects.GLOWING, 100));
}
});1
2
3
4
5
6
7
2
3
4
5
6
7
INFO
Es ist wichtig, dass du den Inhalt des Pakets auf der Serverseite validierst.
In diesem Fall überprüfen wir anhand der Netzwerk-ID, ob die Entität existiert.
java
No lines matched.1
Außerdem muss es sich bei der Zielentität um eine lebende Entität handeln, und wir beschränken die Reichweite der Zielentität vom Spieler auf 5.
java
No lines matched.1
Wenn ein Spieler nun versucht, eine giftige Kartoffel auf einer lebenden Entität zu verwenden, wird der Leuchten-Effekt auf diese angewendet.

















