🇩🇪 Deutsch (German)
🇩🇪 Deutsch (German)
Erscheinungsbild
🇩🇪 Deutsch (German)
🇩🇪 Deutsch (German)
Erscheinungsbild
Diese Seite ist für folgende Version geschrieben:
1.21.4
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.
Die Bedeutung von Networking lässt sich anhand eines einfachen Code-Beispiels verdeutlichen.
WARNING
Der nachstehende Code dient nur zu Demonstrationszwecken.
Angenommen, du hättest einen Zauberstab, der den Block, den du ansiehst, hervorhebt – und das für alle nahegelegenen Spieler sichtbar macht:
public class HighlightingWandItem extends Item {
public HighlightingWandItem(Item.Settings settings) {
super(settings);
}
public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
BlockPos target = ...
// BAD CODE: DON'T EVER DO THIS!
ClientBlockHighlighting.highlightBlock(MinecraftClient.getInstance(), target);
return super.use(world, user, hand);
}
}
Beim Testen wirst du sehen, dass ein Blitz beschworen wird und nichts abstürzt. Wenn du jetzt deinem Freund den Mod zeigen willst, startest du einen dedizierten Server und lädst deinen Freund mit dem installierten Mod ein.
Du verwendest das Item und der Server stürzt ab. Im Absturzprotokoll wird wahrscheinlich ein ähnlicher Fehler wie dieser angezeigt:
[Server thread/FATAL]: Error executing task on Server
java.lang.RuntimeException: Cannot load class net.minecraft.client.MinecraftClient in environment type SERVER
Der Code ruft eine Logik auf, die nur in der Client-Distribution von Minecraft vorhanden ist. Der Grund für Mojang, das Spiel auf diese Weise zu verteilen, ist die Verringerung der Größe der Minecraft Server JAR-Datei. Es gibt eigentlich keinen Grund, eine ganze Rendering-Engine einzubinden, wenn dein eigener Rechner die Welt rendert.
In einer Entwicklungsumgebung werden reine Client-Klassen durch die Annotation @Environment(EnvType.CLIENT)
gekennzeichnet.
Um dieses Problem zu beheben, musst du verstehen, wie der Spiel-Client und der dedizierte Server kommunizieren:
Das obige Diagramm zeigt, dass der Spiel-Client und der dedizierte Server separate Systeme sind, die über_Pakete_ verbunden sind. Pakete können Daten enthalten, die wir als Payload bezeichnen.
Diese Paketbrücke besteht nicht nur zwischen einem Spielclient und einem dedizierten Server, sondern auch zwischen deinem Client und einem anderen über LAN verbundenen Client. Die Paketbrücke ist auch im Einzelspielermodus vorhanden. Der Grund dafür ist, dass der Spielclient eine spezielle integrierte Serverinstanz zum Ausführen des Spiels ausführt.
Die Verbindung zu einem Server über LAN oder den Einzelspielermodus kann auch so behandelt werden, als wäre der Server ein entfernter, dedizierter Server; Dein Spielclient kann also nicht direkt auf die Serverinstanz zugreifen.
Dies kann durch das Erstellen eines Java Record
mit einem BlockPos
-Parameter, der CustomPayload
implementiert, gelöst werden.
public record SummonLightningS2CPayload(BlockPos pos) implements CustomPayload {
public static final Identifier SUMMON_LIGHTNING_PAYLOAD_ID = Identifier.of(FabricDocsReference.MOD_ID, "summon_lightning");
public static final CustomPayload.Id<SummonLightningS2CPayload> ID = new CustomPayload.Id<>(SUMMON_LIGHTNING_PAYLOAD_ID);
public static final PacketCodec<RegistryByteBuf, SummonLightningS2CPayload> CODEC = PacketCodec.tuple(BlockPos.PACKET_CODEC, SummonLightningS2CPayload::pos, SummonLightningS2CPayload::new);
@Override
public Id<? extends CustomPayload> getId() {
return ID;
}
}
Zugleich haben wir folgendes definiert:
Identifier
, der zur Identifizierung des Payload unseres Pakets verwendet wird. In diesem Beispiel lautet der Bezeichner fabric-docs-reference:summon_lightning
.public static final Identifier SUMMON_LIGHTNING_PAYLOAD_ID = Identifier.of(FabricDocsReference.MOD_ID, "summon_lightning");
CustomPayload.Id
zur eindeutigen Identifizierung dieses benutzdefinierten Payloads. Wir werden auf diese ID sowohl in unserem allgemeinen als auch in unserem Client-Code verweisen.public static final CustomPayload.Id<SummonLightningS2CPayload> ID = new CustomPayload.Id<>(SUMMON_LIGHTNING_PAYLOAD_ID);
PacketCodec
, damit das Spiel weiß, wie es den Inhalt des Pakets serialisieren/deserialisieren kann.public static final PacketCodec<RegistryByteBuf, SummonLightningS2CPayload> CODEC = PacketCodec.tuple(BlockPos.PACKET_CODEC, SummonLightningS2CPayload::pos, SummonLightningS2CPayload::new);
Wir haben auch getId
überschrieben, um unsere Payload-ID zurückzugeben.
@Override
public Id<? extends CustomPayload> getId() {
return ID;
}
Bevor wir ein Paket mit unserem benutzerdefinierten Payload senden, müssen wir ihn registrieren.
INFO
S2C
und C2S
sind zwei gängige Suffixe, die Server-to-Client und Client-to-Server bedeuten.
Dies kann in unserem gemeinsamen Initialisierers mit Hilfe von PayloadTypeRegistry.playS2C().register
erfolgen, das eine CustomPayload.Id
und einen PacketCodec
entgegennimmt.
PayloadTypeRegistry.playS2C().register(SummonLightningS2CPayload.ID, SummonLightningS2CPayload.CODEC);
Eine ähnliche Methode existiert, um Client-to-Server Payloads zu registrieren: PayloadTypeRegistry.playC2S().register
.
Um ein Paket mit unserem benutzerdefinierten Payload zu senden, können wir ServerPlayNetworking.send
verwenden, das eine ServerPlayerEntity
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 der Serverwelt senden.
public class LightningTaterItem extends Item {
public LightningTaterItem(Settings settings) {
super(settings);
}
@Override
public ActionResult use(World world, PlayerEntity user, Hand hand) {
if (world.isClient()) {
return ActionResult.PASS;
}
SummonLightningS2CPayload payload = new SummonLightningS2CPayload(user.getBlockPos());
for (ServerPlayerEntity player : PlayerLookup.world((ServerWorld) world)) {
ServerPlayNetworking.send(player, payload);
}
return ActionResult.SUCCESS;
}
}
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 isClient
-Prüfung zurückkehren:
if (world.isClient()) {
return ActionResult.PASS;
}
Wir erstellen eine Instanz des Payloads mit der Position des Users:
SummonLightningS2CPayload payload = new SummonLightningS2CPayload(user.getBlockPos());
Schließlich rufen wir die Spieler in der Serverwelt durch PlayerLookup
ab und senden ein Paket an jeden Spieler.
for (ServerPlayerEntity player : PlayerLookup.world((ServerWorld) world)) {
ServerPlayNetworking.send(player, payload);
}
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.
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 eine CustomPayload.Id
und ein PlayPayloadHandler
übergeben wird, der eine funktionale Schnittstelle ist.
In diesem Fall definieren wir die auszulösende Aktion innerhalb der Implementierung der PlayPayloadHandler
-Implementierung (als Lambda-Ausdruck).
ClientPlayNetworking.registerGlobalReceiver(SummonLightningS2CPayload.ID, (payload, context) -> {
ClientWorld world = context.client().world;
if (world == null) {
return;
}
BlockPos lightningPos = payload.pos();
LightningEntity entity = EntityType.LIGHTNING_BOLT.create(world, SpawnReason.TRIGGERED);
if (entity != null) {
entity.setPosition(lightningPos.getX(), lightningPos.getY(), lightningPos.getZ());
world.addEntity(entity);
}
});
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.
BlockPos lightningPos = payload.pos();
Schließlich erstellen wir eine LightningEntity
und fügen sie der Welt hinzu.
LightningEntity entity = EntityType.LIGHTNING_BOLT.create(world, SpawnReason.TRIGGERED);
if (entity != null) {
entity.setPosition(lightningPos.getX(), lightningPos.getY(), lightningPos.getZ());
world.addEntity(entity);
}
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.
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.
public record GiveGlowingEffectC2SPayload(int entityId) implements CustomPayload {
public static final Identifier GIVE_GLOWING_EFFECT_PAYLOAD_ID = Identifier.of(FabricDocsReference.MOD_ID, "give_glowing_effect");
public static final CustomPayload.Id<GiveGlowingEffectC2SPayload> ID = new CustomPayload.Id<>(GIVE_GLOWING_EFFECT_PAYLOAD_ID);
public static final PacketCodec<RegistryByteBuf, GiveGlowingEffectC2SPayload> CODEC = PacketCodec.tuple(PacketCodecs.INTEGER, GiveGlowingEffectC2SPayload::entityId, GiveGlowingEffectC2SPayload::new);
@Override
public Id<? extends CustomPayload> getId() {
return ID;
}
}
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.playC2S().register
.
PayloadTypeRegistry.playC2S().register(GiveGlowingEffectC2SPayload.ID, GiveGlowingEffectC2SPayload.CODEC);
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 isClient()
, um sicherzustellen, dass die Aktion nur auf dem logischen Client ausgelöst wird.
UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> {
if (!world.isClient()) {
return ActionResult.PASS;
}
ItemStack usedItemStack = player.getStackInHand(hand);
if (entity instanceof LivingEntity && usedItemStack.isOf(Items.POISONOUS_POTATO) && hand == Hand.MAIN_HAND) {
GiveGlowingEffectC2SPayload payload = new GiveGlowingEffectC2SPayload(hitResult.getEntity().getId());
ClientPlayNetworking.send(payload);
return ActionResult.SUCCESS;
}
return ActionResult.PASS;
});
Wir erstellen eine Instanz unseres GiveGlowingEffectC2SPayload
mit den notwendigen Argumenten. In diesem Fall, der Netzwerk ID der Zielentität.
GiveGlowingEffectC2SPayload payload = new GiveGlowingEffectC2SPayload(hitResult.getEntity().getId());
Schließlich senden wir ein Paket an den Server, indem wir ClientPlayNetworking.send
mit der Instanz unseres GiveGlowingEffectC2SPayload
aufrufen.
ClientPlayNetworking.send(payload);
Dies kann im gemeinsamen Initialisierer geschehen, indem man ServerPlayNetworking.registerGlobalReceiver
aufruft und eine CustomPayload.Id
und einen PlayPayloadHandler
übergibt.
ServerPlayNetworking.registerGlobalReceiver(GiveGlowingEffectC2SPayload.ID, (payload, context) -> {
Entity entity = context.player().getWorld().getEntityById(payload.entityId());
if (entity instanceof LivingEntity livingEntity && livingEntity.isInRange(context.player(), 5)) {
livingEntity.addStatusEffect(new StatusEffectInstance(StatusEffects.GLOWING, 100));
}
});
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.
Entity entity = context.player().getWorld().getEntityById(payload.entityId());
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.
if (entity instanceof LivingEntity livingEntity && livingEntity.isInRange(context.player(), 5)) {
Wenn ein Spieler nun versucht, eine giftige Kartoffel auf einer lebenden Entität zu verwenden, wird der Leuchten-Effekt auf diese angewendet.