User Tools

Site Tools


ko_kr:tutorial:networking

~참고: 이 페이지는 이전 네트워킹 페이지를 대체했습니다. 여기 설명된 새로운 네트워킹 API를 사용하는 것이 좋습니다. 이전 페이지는 여기에서 찾을 수 있습니다.

1.20.5에 도입된 최신 네트워킹 API에 대해서는 아래의 별도 문서를 읽어보세요.

네트워킹

마인크래프트의 네트워킹은 클라이언트와 서버가 서로 통신할 수 있도록 합니다. 네트워킹은 광범위한 주제이므로 이 페이지는 몇 가지 범주로 나누어져 있습니다.

예제: 왜 네트워킹이 중요한가?

네트워킹의 중요성은 간단한 코드 예제로 보여줄 수 있습니다. 이 코드는 절대 사용해서는 안 되며 네트워킹이 왜 중요한지를 설명하기 위해 여기에 있습니다.

예를 들어, 당신이 바라보고 있는 블록을 모든 주변 플레이어에게 강조 표시하는 지팡이가 있다고 가정해 봅시다.

  1. public class HighlightingWandItem extends Item {
  2. public HighlightingWand(Item.Settings settings) {
  3. super(settings)
  4. }
  5.  
  6. public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
  7. // 사용자가 바라보고 있는 블록을 찾기 위해 레이캐스트
  8. BlockPos target = ...
  9.  
  10. // 잘못된 코드: 절대 이렇게 하지 마세요!
  11. ClientBlockHighlighting.highlightBlock(MinecraftClient.getInstance(), target);
  12. return super.use(world, user, hand);
  13. }
  14. }

테스트해 보면, 당신이 바라보고 있는 블록이 강조 표시되며 아무런 충돌도 발생하지 않습니다. 이제 친구에게 모드를 보여주고 싶어 전용 서버를 실행하고 친구를 초대합니다. 아이템을 사용하면 서버가 충돌합니다… 충돌 로그에서 다음과 같은 오류를 볼 수 있습니다:

[Server thread/FATAL]: Error executing task on Server
java.lang.RuntimeException: Cannot load class net.minecraft.client.MinecraftClient in environment type SERVER

왜 서버가 충돌하는가?

코드는 마인크래프트 클라이언트 배포판에만 있는 로직을 호출합니다. 모장이 게임을 이렇게 배포하는 이유는 마인크래프트 서버 jar 파일의 크기를 줄이기 위해서입니다. 자신의 컴퓨터가 월드를 렌더링할 때 전체 렌더링 엔진을 포함할 이유가 없습니다. 개발 환경에서 클라이언트 전용 클래스는 @Environment(EnvType.CLIENT) 주석으로 표시됩니다.

충돌을 어떻게 고치는가?

이 문제를 해결하려면, 마인크래프트가 게임 클라이언트와 전용 서버 간에 어떻게 통신하는지 이해해야 합니다.

위의 다이어그램은 게임 클라이언트와 전용 서버가 패킷을 사용하여 연결된 별도의 시스템임을 보여줍니다. 이 패킷 브리지는 게임 클라이언트와 전용 서버 간뿐만 아니라 LAN을 통해 연결된 다른 클라이언트 간에도 존재합니다. 이 패킷 브리지는 싱글플레이어에서도 존재합니다! 이는 게임 클라이언트가 게임을 실행하기 위해 특수한 통합 서버 인스턴스를 생성하기 때문입니다. 세 가지 연결 유형의 주요 차이점은 아래 표에 나와 있습니다:

연결 유형 게임 클라이언트 접근 권한
전용 서버에 연결됨 없음 → 서버 충돌
LAN을 통해 연결됨 있음 → 호스트 게임 클라이언트 아님
싱글플레이어 (또는 LAN 호스트) 있음 → 완전한 접근 권한

서버와 세 가지 다른 방식으로 통신하는 것이 복잡하게 보일 수 있습니다. 그러나 게임 클라이언트와 세 가지 다른 방식으로 통신할 필요는 없습니다. 모든 세 연결 유형이 패킷을 사용하여 게임 클라이언트와 통신하기 때문에 항상 전용 서버에서 실행되는 것처럼 게임 클라이언트와 통신하면 됩니다. LAN 또는 싱글플레이어 서버에 대한 연결도 서버가 원격 전용 서버인 것처럼 처리할 수 있습니다. 따라서 게임 클라이언트는 서버 인스턴스에 직접 접근할 수 없습니다.

네트워킹 소개

우선, 위의 예제 코드에서 문제를 해결해야 합니다. 클라이언트와 통신하기 위해 패킷을 사용하고 있으므로, 액션이 서버에서 시작될 때만 패킷이 전송되도록 해야 합니다.

게임 클라이언트로 패킷 보내기

  1. public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
  2. // 논리적 서버에서 사용 액션을 처리하고 있는지 확인
  3. if (world.isClient()) return super.use(world, user, hand);
  4.  
  5. // 사용자가 바라보고 있는 블록을 찾기 위해 레이캐스트
  6. BlockPos target = ...
  7.  
  8. // 잘못된 코드: 절대 이렇게 하지 마세요!
  9. ClientBlockHighlighting.highlightBlock(MinecraftClient.getInstance(), target);
  10. return TypedActionResult.success(user.getStackInHand(hand));
  11. }

다음으로, 게임 클라이언트로 패킷을 보내야 합니다. 먼저, 패킷을 식별하는 데 사용될 Identifier를 정의해야 합니다. 이 예제에서 우리의 Identifier는 wiki_example:highlight_block입니다. 패킷을 게임 클라이언트로 보내기 위해 어떤 플레이어의 게임 클라이언트가 패킷을 받을지 지정해야 합니다. 액션이 논리적 서버에서 발생하므로, playerServerPlayerEntity로 업캐스트할 수 있습니다.

public class TutorialNetworkingConstants {
    // 나중에 참조할 수 있도록 패킷의 ID를 저장합니다
    public static final Identifier HIGHLIGHT_PACKET_ID = new Identifier("wiki_example", "highlight_block");
}

플레이어에게 패킷을 보내기 위해 ServerPlayNetworking 클래스의 몇 가지 메서드를 사용할 것입니다. 이 클래스에서 사용할 메서드는 다음과 같습니다:

public static void send(ServerPlayerEntity player, Identifier channelName, PacketByteBuf buf) {
    ...

이 메서드의 player는 패킷을 받을 플레이어입니다. 채널 이름은 이전에 정한 패킷을 식별할 Identifier입니다. PacketByteBuf는 패킷의 데이터를 저장합니다. 나중에 buf를 통해 패킷의 페이로드에 데이터를 쓰는 방법을 설명하겠습니다.

지금은 패킷에 데이터를 쓰지 않으므로 빈 페이로드로 패킷을 보낼 것입니다. 빈 페이로드로 buf를 만들기 위해 PacketByteBufs.empty()를 사용할 수 있습니다.

  1. ....
  2. ServerPlayNetworking.send((ServerPlayerEntity) user, TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, PacketByteBufs.empty());
  3. return TypedActionResult.success(user.getHandStack(hand));
  4. }

패킷을 게임 클라이언트로 보냈지만, 클라이언트는 패킷을 받는 방법을 알지 못하므로 아무 일도 일어나지 않습니다. 게임 클라이언트에서 패킷을 받는 방법은 아래에 나와 있습니다:

게임 클라이언트에서 패킷 받기

서버에서 게임 클라이언트로 패킷을 받으려면, 모드가 들어오는 패킷을 처리하는 방법을 지정해야 합니다. 클라이언트 진입점에서 ClientPlayNetworking.registerGlobalReceiver(Identifier channelName, ChannelHandler channelHandler)를 사용하여 패킷의 수신자를 등록합니다.

Identifier는 클라이언트로 패킷을 보낼 때 사용한 동일한 Identifier여야 합니다. ChannelHandler는 패킷이 처리되는 방식을 구현하는 기능적 인터페이스입니다. ChannelHandlerClientPlayNetworking의 중첩 인터페이스여야 합니다

아래 예제는 람다로 재생 채널 핸들러를 구현한 것입니다:

  1. ClientPlayNetworking.registerGlobalReceiver(TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, (client, handler, buf, responseSender) -> {
  2. ...
  3. });

그러나 즉시 강조 표시 상자를 그릴 수는 없습니다. 이는 수신자가 netty 이벤트 루프에서 호출되기 때문입니다. 이벤트 루프는 다른 스레드에서 실행되며, 강조 표시 상자를 렌더 스레드에서 그려야 합니다.

강조 표시 상자를 그리기 위해 게임 클라이언트에서 작업을 예약해야 합니다. 일반적으로 클라이언트의 execute 메서드를 사용하여 작업을 실행합니다:

  1. ClientPlayNetworking.registerGlobalReceiver(TutorialNetworkingConstants.H
  2.  
  3. IGHLIGHT_PACKET_ID, (client, handler, buf, responseSender) -> {
  4. client.execute(() -> {
  5. // 이 람다 내의 모든 것은 렌더 스레드에서 실행됩니다
  6. });
  7. });

강조할 블록의 위치를 알려주지 않았다는 점을 눈치챘을 것입니다. 이 데이터를 패킷 바이트 buf에 쓸 수 있습니다. 아이템의 use 메서드에서 PacketByteBufs.empty()를 보내는 대신, 새 패킷 바이트 buf를 만들어 보내야 합니다.

  1. PacketByteBuf buf = PacketByteBufs.create();

다음으로, 패킷 바이트 buf에 데이터를 써야 합니다. 데이터를 쓰는 순서대로 읽어야 한다는 점에 유의해야 합니다.

  1. PacketByteBuf buf = PacketByteBufs.create();
  2.  
  3. buf.writeBlockPos(target);

그 후, send 메서드를 통해 buf 필드를 보냅니다.

게임 클라이언트에서 이 블록 위치를 읽으려면 PacketByteBuf.readBlockPos()를 사용할 수 있습니다.

네트워크 스레드에서 클라이언트 스레드로 작업을 예약하기 전에 패킷의 모든 데이터를 읽어야 합니다. 클라이언트 스레드에서 데이터를 읽으려고 하면 ref count 관련 오류가 발생합니다. 클라이언트 스레드에서 데이터를 읽어야 한다면 데이터를 retain()한 후 클라이언트 스레드에서 읽어야 합니다. 데이터를 retain()하면 더 이상 필요 없을 때 release()해야 합니다.

결국 클라이언트의 핸들러는 다음과 같이 보일 것입니다:

  1. ClientPlayNetworking.registerGlobalReceiver(TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, (client, handler, buf, responseSender) -> {
  2. // 이벤트 루프에서 패킷 데이터를 읽습니다
  3. BlockPos target = buf.readBlockPos();
  4.  
  5. client.execute(() -> {
  6. // 이 람다 내의 모든 것은 렌더 스레드에서 실행됩니다
  7. ClientBlockHighlighting.highlightBlock(client, target);
  8. });
  9. });

서버로 패킷 보내기 및 서버에서 패킷 받기

서버로 패킷 보내기와 서버에서 패킷 받기는 클라이언트에서 하는 방식과 매우 유사합니다. 그러나 몇 가지 중요한 차이점이 있습니다.

첫째, 서버로 패킷을 보내는 것은 ClientPlayNetworking.send를 통해 이루어집니다. 서버에서 패킷을 받는 것은 클라이언트에서 패킷을 받는 것과 유사하며, ServerPlayNetworking.registerGlobalReceiver(Identifier channelName, ChannelHandler channelHandler) 메서드를 사용합니다. 서버 네트워킹의 ChannelHandler는 패킷을 보낸 ServerPlayerEntity(플레이어)를 player 매개변수를 통해 전달합니다.

트래킹 개념과 왜 당신만 강조된 블록을 볼 수 있는지에 대해

이제 하이라이팅 지팡이가 제대로 네트워킹을 사용하여 전용 서버가 충돌하지 않도록 했으므로, 서버에 친구를 다시 초대하여 하이라이팅 지팡이를 자랑합니다. 지팡이를 사용하면 블록이 클라이언트에서 강조 표시되고 서버는 충돌하지 않습니다. 그러나 친구는 강조 표시된 블록을 볼 수 없습니다. 이는 의도된 코드입니다. 이 문제를 해결하려면 아이템의 사용 코드를 살펴보겠습니다:

  1. public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
  2. // 논리적 서버에서 사용 액션을 처리하고 있는지 확인
  3. if (world.isClient()) return super.use(world, user, hand);
  4.  
  5. // 사용자가 바라보고 있는 블록을 찾기 위해 레이캐스트
  6. BlockPos target = ...
  7. PacketByteBuf buf = PacketByteBufs.create();
  8. buf.writeBlockPos(target);
  9.  
  10. ServerPlayNetworking.send((ServerPlayerEntity) user, TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, buf);
  11. return TypedActionResult.success(user.getHandStack(hand));
  12. }

아이템이 패킷을 사용한 플레이어에게만 보내는 것을 알 수 있습니다. 이를 해결하기 위해, PlayerLookup의 유틸리티 메서드를 사용하여 강조된 블록을 볼 수 있는 모든 플레이어를 가져올 수 있습니다.

강조 표시가 발생할 위치를 알고 있으므로, PlayerLookup.tracking(ServerWorld world, BlockPos pos)를 사용하여 해당 위치를 볼 수 있는 모든 플레이어의 컬렉션을 가져올 수 있습니다. 그런 다음 반환된 컬렉션의 모든 플레이어를 반복하여 각 플레이어에게 패킷을 보냅니다:

  1. public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
  2. // 논리적 서버에서 사용 액션을 처리하고 있는지 확인
  3. if (world.isClient()) return super.use(world, user, hand);
  4.  
  5. // 사용자가 바라보고 있는 블록을 찾기 위해 레이캐스트
  6. BlockPos target = ...
  7. PacketByteBuf buf = PacketByteBufs.create();
  8. buf.writeBlockPos(target);
  9.  
  10. // 월드의 위치를 추적하는 모든 플레이어를 반복하여 각 플레이어에게 패킷을 보냅니다
  11. for (ServerPlayerEntity player : PlayerLookup.tracking((ServerWorld) world, target)) {
  12. ServerPlayNetworking.send(player, TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, buf);
  13. }
  14.  
  15. return TypedActionResult.success(user.getHandStack(hand));
  16. }

이 변경 후, 지팡이를 사용하면 친구도 자신의 클라이언트에서 강조된 블록을 볼 수 있습니다.

1.20.5의 네트워킹

1.20.5 이후로 네트워킹 로직이 완전히 재작성되었습니다. 1.20.5에서는 PLAY 단계 네트워킹에 RegistryByteBuf가 사용되며, 커스텀 Payload를 정의해야 합니다. 먼저, BlockPos를 포함하는 페이로드를 정의합니다:

public record BlockHighlightPayload(BlockPos blockPos) implements CustomPayload {
    public static final CustomPayload.Id<BlockHighlightPayload> ID = new CustomPayload.Id<>(TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID);
    public static final PacketCodec<RegistryByteBuf, BlockHighlightPayload> CODEC = PacketCodec.tuple(BlockPos.PACKET_CODEC, BlockHighlightPayload::blockPos, BlockHighlightPayload::new);
    // 더 많은 데이터를 보내야 하는 경우, 적절한 레코드 매개변수를 추가하고 코덱을 변경하십시오:
    // public static final PacketCodec<RegistryByteBuf, BlockHighlightPayload> CODEC = PacketCodec.tuple(
    //         BlockPos.PACKET_CODEC, BlockHighlightPayload::blockPos,
    //         PacketCodecs.INTEGER, BlockHighlightPayload::myInt,
    //         Uuids.PACKET_CODEC, BlockHighlightPayload::myUuid,
    //         BlockHighlightPayload::new
    // );
 
    @Override
    public CustomPayload.Id<? extends CustomPayload> getId() {
        return ID;
    }
}

그런 다음, 수신자를 다음과 같이 등록합니다:

// 공통 초기화 메서드에서
PayloadTypeRegistry.playS2C().register(BlockHighlightPayload.ID, BlockHighlightPayload.CODEC);
 
// 클라이언트 전용 초기화 메서드에서
ClientPlayNetworking.registerGlobalReceiver(BlockHighlightPayload.ID, (payload, context) -> {
    context.client().execute(() -> {
        ClientBlockHighlighting.highlightBlock(client, payload.blockPos());
    });
});

이제, 서버 측에서 플레이어에게 패킷을 다음과 같이 보낼 수 있습니다:

    public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
        if (world.isClient()) return super.use(world, user, hand);
 
        // ...
 
        for (ServerPlayerEntity player : PlayerLookup.tracking((ServerWorld) world, target)) {
            ServerPlayNetworking.send(player, new BlockHighlightPayload(target));
        }
 
        return TypedActionResult.success(user.getHandStack(hand));
    }
ko_kr/tutorial/networking.txt · Last modified: 2024/07/01 12:42 by lunarec