NeoForge 26.1 Docs
  • 문서
  • 노트
NeoForge 26.1
NeoForge 26.1
v26.1
학습 진도
0 / 77 챕터 완료방문 0
마스터 트랙 #2: 차원 모드 프로젝트 셋업차원 등록 — 데이터팩 3종 JSON포탈 블록 + 점화 아이템포탈 프레임 인식 로직차원 진입·복귀 로직차원 룰셋 — 낮/밤, 침대, 스카이박스차원 모드 마무리 + JAR 빌드
마스터차원

포탈 프레임 인식 로직

PortalShape 헬퍼 클래스로 흑요석 프레임을 탐색·검증하고, MagicIgniter에서 isValidFrame을 호출해 유효한 프레임에만 포탈 블록을 채우는 로직을 구현합니다.

포탈 프레임 인식 로직

이 챕터에서는 PortalShape 헬퍼 클래스를 구현하여 흑요석 프레임을 자동으로 탐색·검증하고, MagicIgniter에서 이를 호출해 완전한 프레임에만 포탈 블록을 채우도록 업데이트합니다.


1. PortalShape 설계 원칙

왜 별도 클래스인가?

포탈 형태 인식 로직을 MagicPortalBlock이나 MagicIgniter 안에 직접 구현하면 단일 책임 원칙(SRP)이 무너집니다. PortalShape는 오직 프레임 인식만 담당합니다.

역할클래스
프레임 탐색·검증PortalShape (이 챕터)
포탈 블록 등록MagicPortalBlock
점화 트리거MagicIgniter
차원 이동entityInside (04-dimension-entry)

좌표 설계 원칙

⚠️ 부동소수 좌표 사용 금지

// ❌ 부동소수로 블록 좌표 계산 → 부정확
double x = pos.getX() + 0.5;
level.setBlock(new BlockPos((int) x, y, z), state, 11);
 
// ✅ BlockPos 사용 — 정수 블록 좌표
BlockPos inner = cornerPos.offset(x, y, 0);
level.setBlock(inner, state, 11);

블록 좌표는 항상 BlockPos (정수) 로 계산합니다. 부동소수 변환은 반올림 오차로 인해 블록 위치가 어긋납니다.


2. PortalShape 구현

// examplemod-master-projects/dimension/src/main/java/com/example/master/dimension/portal/PortalShape.java
package com.example.master.dimension.portal;
 
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
 
/**
 * 포탈 프레임 형태 인식 유틸리티.
 *
 * <p>사용 흐름:
 * 1. findShape(Level, BlockPos) — 주어진 블록 주변에서 유효한 프레임을 찾아 PortalShape 반환
 * 2. isValidFrame(Level, BlockPos) — 단순 유효성 검사 (불리언)
 * 3. fillInterior(Level, BlockState) — 내부 공기 슬롯을 포탈 블록으로 채움
 */
public class PortalShape {
 
    /** 프레임 최소 크기 (내부 포함 전체) */
    public static final int MIN_SIZE = 3;
    /** 프레임 최대 크기 (내부 포함 전체) */
    public static final int MAX_SIZE = 21;
 
    /** 좌측 하단 코너 좌표 (프레임 블록 위치) */
    private final BlockPos cornerPos;
    /** 프레임 전체 너비 (코너 포함) */
    private final int width;
    /** 프레임 전체 높이 (코너 포함) */
    private final int height;
 
    private PortalShape(BlockPos cornerPos, int width, int height) {
        this.cornerPos = cornerPos;
        this.width = width;
        this.height = height;
    }
 
    // ── 공개 API ──────────────────────────────────────────────────────────────
 
    /**
     * 주어진 위치 주변에서 유효한 포탈 프레임 형태를 탐색합니다.
     *
     * @param level 현재 월드
     * @param pos   프레임 블록 또는 내부 블록 위치
     * @return 유효한 프레임이면 PortalShape, 없으면 null
     */
    public static PortalShape findShape(Level level, BlockPos pos) {
        BlockPos corner = findCorner(level, pos);
        if (corner == null) return null;
 
        int w = measureWidth(level, corner);
        int h = measureHeight(level, corner);
 
        if (w < MIN_SIZE || w > MAX_SIZE) return null;
        if (h < MIN_SIZE || h > MAX_SIZE) return null;
        if (!isFrameComplete(level, corner, w, h)) return null;
 
        return new PortalShape(corner, w, h);
    }
 
    /**
     * 주어진 위치에 유효한 포탈 프레임이 있는지 빠르게 확인합니다.
     */
    public static boolean isValidFrame(Level level, BlockPos pos) {
        return findShape(level, pos) != null;
    }
 
    /**
     * 프레임 내부의 Air 슬롯을 portalState로 채웁니다.
     */
    public void fillInterior(Level level, BlockState portalState) {
        for (int x = 1; x < width - 1; x++) {
            for (int y = 1; y < height - 1; y++) {
                BlockPos inner = cornerPos.offset(x, y, 0);
                if (level.getBlockState(inner).isAir()) {
                    level.setBlock(inner, portalState, 11); // UPDATE | NOTIFY_CLIENTS
                }
            }
        }
    }
 
    // ── 내부 헬퍼 ─────────────────────────────────────────────────────────────
 
    /** 좌측 하단 코너(프레임 블록)를 찾습니다. */
    private static BlockPos findCorner(Level level, BlockPos pos) {
        BlockPos current = pos;
        for (int i = 0; i < MAX_SIZE; i++) {
            if (isFrameBlock(level, current)) break;
            current = current.below();
            if (i == MAX_SIZE - 1) return null;
        }
        for (int i = 0; i < MAX_SIZE; i++) {
            if (!isFrameBlock(level, current.west())) break;
            current = current.west();
        }
        return current;
    }
 
    /** 코너에서 오른쪽(+X)으로 프레임 너비를 측정합니다. */
    private static int measureWidth(Level level, BlockPos corner) {
        int w = 0;
        for (int x = 0; x <= MAX_SIZE; x++) {
            if (!isFrameBlock(level, corner.east(x))) break;
            w++;
        }
        return w;
    }
 
    /** 코너에서 위쪽(+Y)으로 프레임 높이를 측정합니다. */
    private static int measureHeight(Level level, BlockPos corner) {
        int h = 0;
        for (int y = 0; y <= MAX_SIZE; y++) {
            if (!isFrameBlock(level, corner.above(y))) break;
            h++;
        }
        return h;
    }
 
    /** 프레임 무결성 검사: 4변 + 내부 Air 확인 */
    private static boolean isFrameComplete(Level level, BlockPos corner, int w, int h) {
        for (int x = 0; x < w; x++) {
            if (!isFrameBlock(level, corner.east(x))) return false;
            if (!isFrameBlock(level, corner.east(x).above(h - 1))) return false;
        }
        for (int y = 0; y < h; y++) {
            if (!isFrameBlock(level, corner.above(y))) return false;
            if (!isFrameBlock(level, corner.east(w - 1).above(y))) return false;
        }
        for (int x = 1; x < w - 1; x++) {
            for (int y = 1; y < h - 1; y++) {
                if (!level.getBlockState(corner.east(x).above(y)).isAir()) return false;
            }
        }
        return true;
    }
 
    /** 흑요석 또는 운명의 흑요석인지 확인합니다. */
    private static boolean isFrameBlock(Level level, BlockPos pos) {
        var block = level.getBlockState(pos).getBlock();
        return block == Blocks.OBSIDIAN || block == Blocks.CRYING_OBSIDIAN;
    }
 
    // ── 게터 ─────────────────────────────────────────────────────────────────
    public BlockPos getCornerPos() { return cornerPos; }
    public int getWidth()          { return width; }
    public int getHeight()         { return height; }
}

3. 유효한 프레임 조건

조건값
최소 크기3×3 (내부 1×1)
최대 크기21×21
프레임 재질흑요석(Blocks.OBSIDIAN) 또는 운명의 흑요석(Blocks.CRYING_OBSIDIAN)
내부모두 Air여야 함 (이미 포탈 블록이 있으면 거부)

탐색 알고리즘 흐름

findShape(level, pos)
  │
  ├─ findCorner(level, pos)
  │     ├─ 아래(−Y)로 이동 → 프레임 블록 행 탐색
  │     └─ 왼쪽(−X)으로 이동 → 좌측 하단 코너 확정
  │
  ├─ measureWidth()  : 코너에서 +X 방향으로 프레임 블록 수 세기
  ├─ measureHeight() : 코너에서 +Y 방향으로 프레임 블록 수 세기
  │
  ├─ 크기 범위 검사 (3 ≤ w,h ≤ 21)
  │
  └─ isFrameComplete()
        ├─ 바닥 행 · 천장 행 프레임 블록 검사
        ├─ 좌측 열 · 우측 열 프레임 블록 검사
        └─ 내부 전체 Air 검사

4. MagicIgniter 업데이트

이전 챕터의 단순 배치 로직을 PortalShape.isValidFrame으로 교체합니다.

// examplemod-master-projects/dimension/src/main/java/com/example/master/dimension/item/MagicIgniter.java
package com.example.master.dimension.item;
 
import com.example.master.dimension.MasterDimensionMod;
import com.example.master.dimension.portal.PortalShape;
import net.minecraft.core.BlockPos;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
 
public class MagicIgniter extends Item {
 
    public MagicIgniter(Properties props) {
        super(props);
    }
 
    @Override
    public InteractionResult useOn(UseOnContext context) {
        Level level = context.getLevel();
        BlockPos pos = context.getClickedPos();
 
        // 클라이언트 측은 시각 효과만 — 서버에서 실제 처리
        if (level.isClientSide()) return InteractionResult.SUCCESS;
 
        // PortalShape.isValidFrame으로 프레임 유효성 검사
        if (!PortalShape.isValidFrame(level, pos)) {
            return InteractionResult.PASS; // 프레임 불완전 → 점화 거부
        }
 
        // 유효한 프레임 → 형태 찾아 내부 채움
        PortalShape shape = PortalShape.findShape(level, pos);
        if (shape == null) return InteractionResult.PASS;
 
        BlockState portalState = MasterDimensionMod.MAGIC_PORTAL.get().defaultBlockState();
        shape.fillInterior(level, portalState);
 
        level.playSound(null, pos, SoundEvents.FLINTANDSTEEL_USE,
                SoundSource.BLOCKS, 1.0f, 1.0f);
        return InteractionResult.SUCCESS;
    }
}

useOn 흐름 (업데이트 후)

우클릭 (서버측)
  │
  ├─ isClientSide() → SUCCESS (클라이언트 즉시 피드백)
  │
  ├─ PortalShape.isValidFrame(level, pos)
  │     ├─ false → PASS (프레임 불완전, 아무 동작 없음)
  │     └─ true  ↓
  │
  ├─ PortalShape.findShape(level, pos)
  │     └─ shape.fillInterior(level, portalState)
  │           └─ 내부 Air 슬롯마다 MAGIC_PORTAL 블록 배치
  │
  └─ 부싯돌 사운드 재생 → SUCCESS

5. 커스텀 프레임 재질 확장

기본 구현은 흑요석/운명의 흑요석만 허용합니다. 커스텀 블록으로 확장하려면 isFrameBlock 메서드를 수정합니다.

// PortalShape.java
private static boolean isFrameBlock(Level level, BlockPos pos) {
    var block = level.getBlockState(pos).getBlock();
    // 기본: 흑요석
    if (block == Blocks.OBSIDIAN || block == Blocks.CRYING_OBSIDIAN) return true;
    // 확장: 커스텀 프레임 블록 추가
    // if (block == MasterDimensionMod.MY_FRAME_BLOCK.get()) return true;
    return false;
}

6. 인게임 검증 안내

시나리오예상 동작
3×3 흑요석 프레임 완성 → MagicIgniter 우클릭내부 1×1 슬롯에 포탈 블록 배치 + 사운드
프레임 한 블록 누락 → MagicIgniter 우클릭아무 동작 없음 (PASS 반환)
내부에 다른 블록 존재 → MagicIgniter 우클릭점화 거부 (isAir 검사 실패)
4×5 흑요석 프레임 → MagicIgniter 우클릭내부 2×3 슬롯 전체에 포탈 블록 배치

ℹ️ 다음 챕터에서 추가

포탈 블록에 들어가면 차원이 이동하는 로직(entityInside)은 04-dimension-entry에서 구현됩니다.


다음 단계

  1. 04-dimension-entry — MagicPortalBlock.entityInside 오버라이드 + ChangeDimensionEvent 처리로 플레이어 차원 이동
  2. 05-finish-build — 통합 Gradle 빌드 & 인게임 종합 검증

포탈 블록 + 점화 아이템

MagicPortalBlock(Block 상속, 4×5 프레임 내부)과 MagicIgniter(점화 아이템)를 구현하고 DeferredRegister로 등록합니다. 포탈 형태 검사와 차원 이동은 후속 챕터에서 완성됩니다.

차원 진입·복귀 로직

MagicPortalBlock.entityInside 오버라이드로 플레이어가 포탈 블록에 접촉할 때 magic_realm ↔ 오버월드 간 양방향 차원 이동을 구현합니다. 무한 TP 루프 방지 쿨다운 패턴도 다룹니다.

On this page

포탈 프레임 인식 로직1. PortalShape 설계 원칙왜 별도 클래스인가?좌표 설계 원칙2. PortalShape 구현3. 유효한 프레임 조건탐색 알고리즘 흐름4. MagicIgniter 업데이트useOn 흐름 (업데이트 후)5. 커스텀 프레임 재질 확장6. 인게임 검증 안내다음 단계
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