NeoForge 26.1 Docs
  • 문서
  • 노트
NeoForge 26.1
NeoForge 26.1
v26.1
학습 진도
0 / 77 챕터 완료방문 0
마스터 트랙 #3: 기계 모드 프로젝트 셋업분쇄기 블록 등록 — CrusherBlock + facing BlockStateBlockEntity + 인벤토리 — ItemStackHandlerMenu + Screen — GUI 구현화면 위젯 — 진행 바와 슬롯 시각화커스텀 레시피 타입 — Crusher Recipe서버↔클라이언트 동기화 패킷기계 모드 마무리 + JAR 빌드
마스터기계

서버↔클라이언트 동기화 패킷

CustomPacketPayload + StreamCodec으로 CrusherProgressPayload를 설계하고, RegisterPayloadHandlersEvent로 핸들러를 등록하여 분쇄 진행도를 서버에서 클라이언트로 효율적으로 동기화합니다.

서버↔클라이언트 동기화 패킷

이 챕터에서는 분쇄기의 진행 상태(progress)를 서버에서 클라이언트로 동기화하는 패킷을 구현합니다. NeoForge 26.1의 CustomPacketPayload + StreamCodec 패턴을 사용하여 타입 안전한 페이로드를 정의하고, RegisterPayloadHandlersEvent로 핸들러를 등록합니다.


1. 패킷 아키텍처 개요

Minecraft 멀티플레이에서 BlockEntity 데이터는 서버가 관리합니다. 클라이언트 GUI(진행 바)가 올바른 값을 표시하려면 서버가 데이터를 명시적으로 전송해야 합니다.

서버 CrusherBlockEntity
  │  progress 변경
  └─ PacketDistributor.sendToPlayersTrackingChunk(...)
        │
        │  [CrusherProgressPayload] → 네트워크 전송
        │
클라이언트 핸들러
  └─ be.setProgress(payload.progress())
        │
        └─ CrusherScreen 진행 바 갱신

NeoForge 26.1 패킷 시스템 핵심 컴포넌트:

컴포넌트역할
CustomPacketPayload패킷 데이터 컨테이너 인터페이스
StreamCodec바이트 버퍼 직렬화/역직렬화
PayloadRegistrar.playToClient서버→클라이언트 단방향 핸들러 등록
PacketDistributor대상 플레이어 선택 + 전송

2. CrusherProgressPayload

record로 선언하여 불변 데이터 컨테이너를 만듭니다. 두 필드(블록 좌표 + 진행 틱)만 전송하므로 패킷 크기가 최소화됩니다.

// examplemod-master-projects/machine/src/main/java/com/example/master/machine/network/CrusherProgressPayload.java
package com.example.master.machine.network;
 
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.Identifier;
 
public record CrusherProgressPayload(BlockPos pos, int progress)
        implements CustomPacketPayload {
 
    public static final Type<CrusherProgressPayload> TYPE =
            new Type<>(Identifier.fromNamespaceAndPath("master_machine", "crusher_progress"));
 
    public static final StreamCodec<FriendlyByteBuf, CrusherProgressPayload> STREAM_CODEC =
            StreamCodec.composite(
                    BlockPos.STREAM_CODEC, CrusherProgressPayload::pos,
                    ByteBufCodecs.INT, CrusherProgressPayload::progress,
                    CrusherProgressPayload::new);
 
    @Override
    public Type<? extends CustomPacketPayload> type() { return TYPE; }
}

StreamCodec 조합 이해

StreamCodec.composite는 여러 필드를 순서대로 인코딩합니다.

인코딩 (서버→바이트버퍼):
  1. BlockPos.STREAM_CODEC → pos를 VarInt x3로 직렬화 (약 3~9바이트)
  2. ByteBufCodecs.INT     → progress를 4바이트 int로 직렬화

디코딩 (바이트버퍼→클라이언트):
  1. BlockPos 역직렬화
  2. int 역직렬화
  3. CrusherProgressPayload::new 생성자 호출

⚠️ 필드 순서 고정
인코딩과 디코딩의 순서가 반드시 동일해야 합니다. composite 인수 순서를 바꾸면 잘못된 값이 역직렬화됩니다.


3. RegisterPayloadHandlersEvent 등록

MasterMachineMod.java에 MOD 버스 이벤트 핸들러를 추가합니다.

// MasterMachineMod.java (발췌)
import com.example.master.machine.network.CrusherProgressPayload;
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
import net.neoforged.neoforge.network.registration.PayloadRegistrar;
 
@SubscribeEvent
public static void onRegisterPayloads(RegisterPayloadHandlersEvent event) {
    PayloadRegistrar registrar = event.registrar("1");
    registrar.playToClient(
            CrusherProgressPayload.TYPE,
            CrusherProgressPayload.STREAM_CODEC,
            (payload, ctx) -> {
                var level = ctx.player().level();
                if (level.getBlockEntity(payload.pos()) instanceof CrusherBlockEntity be) {
                    be.setProgress(payload.progress());
                }
            });
}

핸들러 실행 컨텍스트

playToClient 핸들러는 클라이언트 메인 스레드에서 실행됩니다. ctx.player().level()이 클라이언트 월드를 반환하므로, 해당 좌표의 BlockEntity를 찾아 setProgress()를 호출하면 바로 GUI가 갱신됩니다.

registrar.playToXxx 메서드방향
playToClient서버 → 클라이언트
playToServer클라이언트 → 서버
playBidirectional양방향

4. CrusherBlockEntity — lastSyncedProgress + setProgress

lastSyncedProgress 필드로 마지막 전송값을 추적합니다. progress가 변경될 때만 패킷을 보냅니다.

// CrusherBlockEntity.java (관련 부분)
int progress = 0;
int lastSyncedProgress = -1;  // 초기값 -1 → 첫 tick에 반드시 동기화됨
 
public void setProgress(int progress) {
    this.progress = progress;  // 클라이언트 측 수신 메서드
}

5. tick에서 변화 감지 후 전송

CrusherBlockEntity.tick() 정적 메서드에서 레시피 처리 후 진행도 변경 시에만 패킷을 전송합니다.

// CrusherBlockEntity.java — tick 메서드 (발췌)
public static void tick(Level level, BlockPos pos, BlockState state, CrusherBlockEntity be) {
    if (level.isClientSide()) return;
 
    // ... 레시피 처리 로직 ...
    
    // 진행 변경 시에만 전송 (매 tick X)
    syncIfChanged(level, pos, be);
}
 
private static void syncIfChanged(Level level, BlockPos pos, CrusherBlockEntity be) {
    if (be.progress != be.lastSyncedProgress) {
        PacketDistributor.sendToPlayersTrackingChunk(
                (ServerLevel) level,
                new ChunkPos(pos),
                new CrusherProgressPayload(pos, be.progress)
        );
        be.lastSyncedProgress = be.progress;
    }
}

sendToPlayersTrackingChunk는 해당 청크를 로드한 모든 플레이어에게 전송합니다. 분쇄기를 보고 있는 모든 플레이어가 동기화됩니다.


6. getTicker 등록 (CrusherBlock)

BlockEntity tick이 호출되려면 CrusherBlock.getTicker()가 필요합니다.

// CrusherBlock.java
@Nullable
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(
        Level level, BlockState state, BlockEntityType<T> type) {
    if (level.isClientSide()) return null;
    if (type == MasterMachineMod.CRUSHER_BE.get()) {
        //noinspection unchecked
        return (BlockEntityTicker<T>) (BlockEntityTicker<CrusherBlockEntity>) CrusherBlockEntity::tick;
    }
    return null;
}

클라이언트에서는 null을 반환해 서버 로직이 클라이언트에서 실행되지 않도록 합니다.


⚠️ 안티패턴 — 매 tick 전송

매 tick 패킷 전송 → 네트워크 폭주

// ❌ 매 tick 전송 (20 packets/sec)
public static void tick(...) {
    PacketDistributor.sendToPlayersTrackingChunk(...);  // 조건 없이 매 tick
}
 
// ✅ 변화 감지 후 전송
if (be.progress != be.lastSyncedProgress) {
    PacketDistributor.sendToPlayersTrackingChunk(...);
    be.lastSyncedProgress = be.progress;
}

20 TPS 환경에서 조건 없이 전송하면 초당 20개의 패킷이 발생합니다. lastSyncedProgress로 변경 감지를 하면 진행 중(tick마다 +1)에는 여전히 패킷이 발생하지만, **아무것도 가공하지 않는 상태(progress == 0)**에서는 패킷이 전송되지 않습니다. 실제 최적화는 UI 렌더링이 허용하는 범위 내에서 전송 주기를 늘리는 것입니다(예: 5 tick마다 1회).

네트워크 대역폭 + 클라이언트 처리 비용 절감


7. 인게임 검증 안내

양 플레이어 동기화를 검증하려면:

  1. 서버 실행 (단일 플레이어 또는 로컬 서버)
  2. 분쇄기 블록 설치 후 입력 슬롯에 재료 투입
  3. 두 번째 플레이어가 같은 청크에서 분쇄기 GUI를 열면 진행 바가 동일하게 움직여야 함
  4. F3 화면에서 "Packets" 카운터로 패킷 발생량 확인 가능

ℹ️ 참고용 스크린샷
인게임 멀티플레이 동기화 검증 단계는 실제 서버 실행 환경이 필요합니다. 본 캡처는 코드 구조 illustration으로만 사용됩니다.


정리

클래스추가 내용
CrusherProgressPayloadCustomPacketPayload record + TYPE + STREAM_CODEC
CrusherBlockEntitylastSyncedProgress 필드 + setProgress() + syncIfChanged()
CrusherBlockgetTicker() — 서버 tick 연결
MasterMachineModonRegisterPayloads — playToClient 핸들러 등록

다음 챕터에서는 전체 모드를 빌드하고 인게임에서 최종 동작을 검증합니다.

커스텀 레시피 타입 — Crusher Recipe

MapCodec + StreamCodec으로 CrusherRecipe 커스텀 레시피 타입을 설계하고, RecipeSerializer·RecipeType을 DeferredRegister로 등록하여 데이터팩 JSON에서 레시피를 정의합니다.

기계 모드 마무리 + JAR 빌드

Crusher 자동화 기계 모드의 전체 코드를 점검하고, Gradle로 JAR를 빌드하여 인게임에서 동작을 최종 검증합니다. 빌드 산출물 구조와 배포 전 체크리스트도 다룹니다.

On this page

서버↔클라이언트 동기화 패킷1. 패킷 아키텍처 개요2. CrusherProgressPayloadStreamCodec 조합 이해3. RegisterPayloadHandlersEvent 등록핸들러 실행 컨텍스트4. CrusherBlockEntity — lastSyncedProgress + setProgress5. tick에서 변화 감지 후 전송6. getTicker 등록 (CrusherBlock)⚠️ 안티패턴 — 매 tick 전송7. 인게임 검증 안내정리
NeoForge 26.1 Docs

NeoForge 26.1 모딩 개발 문서 사이트

GitHubDiscord

문서

  • 문서
  • 노트

GitHub

  • GitHub
  • Discord

© 2026 NeoForge 26.1 Docs. 콘텐츠는 MIT 라이선스로 제공됩니다.

Built with Next.js · Tailwind CSS · shadcn/ui