{{{#!wiki style="margin: -0px -10px -5px; min-height:calc(1.5em + 5px)" {{{#!folding [ 펼치기 · 접기 ] {{{#!wiki style="margin: -5px -1px -11px; word-break:keep-all" | <colbgcolor=darkgreen><colcolor=#fff> 기본 플레이 | |
시스템 | 세계 (시드) · 게임 모드 · 난이도 · 게임 규칙 · 엔딩 · 죽음 메시지 · 스플래시 · 명령어 · NBT · 런처 | |
인게임 | 아이템 · 몹 (플레이어) · 개체 · 날씨 · 차원 · 생물군계 · 구조물 · 마법 부여 · 상태 효과 · 조작법 · 피해 · 업적 | |
도움말 | 튜토리얼 · 팁 (탐험 · 파밍 · 회로) · 재생 가능한 자원 · 브릿징 · PVP · 파쿠르 · 스피드런 · 건축 (맵아트) | |
시리즈 및 매체 | ||
출시 에디션 | 자바 에디션 (업데이트 · 거리 효과) · 베드락 에디션 (업데이트) · 포켓 에디션* · 콘솔 에디션* · 파이 에디션* | |
파생 게임 | 마인크래프트 던전스* · 마인크래프트 레전드* · 마인크래프트 에듀케이션 · 마인크래프트: 스토리 모드* · 마인크래프트 어스* | |
미디어 | OST · 관련 서적 · 레고 · 영화 · 애니메이션 · Minecraft Live · Minecraft Now · Minecraft Monthly | |
유저 콘텐츠 | ||
창작 요소 | 2차 창작 · 망토 · 맵 · 모드 (개발 · 팁 · 모드팩) · 애드온 · 팩 (리소스 팩 · 데이터 팩) · 외부 프로그램 · 핵 | |
멀티 콘텐츠 | 멀티플레이 · 서버 · 플러그인 · Realms · EULA | |
개발 | 개발 기초 · 모드 개발 · 플러그인 개발 | |
기타 | ||
이야깃거리 | 여담 · 커뮤니티 · 사건 사고 · 문제점 · 용어 · 지원 언어 · 머나먼 땅 · 이미테이션 게임 · 히로빈 | |
관련 문서 | 나무위키 마인크래프트 프로젝트 · 마인크래프트로 분류된 문서 · 마인크래프트의 하위 문서 | |
* 표시는 서비스가 종료되었거나 개발이 중단되었다는 표시이다. | }}}}}}}}} |
1. 개요
마인크래프트 플레이의 끝장판. 여기서부터는 단순히 게임을 플레이하기만 하는 게 아니다. 게임의 뿌리를 살펴보고 수정하는 일이다. 당연히 프로그래밍(Java)을 어느정도 할 수 있어야 하고 마인크래프트에 관한 이해도 또한 요구된다. 애드온 등의 타 모드 관련 개발 시에는 해당 모드에 관한 이해도 필요하며, 업데이트가 출시되면 크리에이티브 모드로 게임에 익숙해지는것도 좋은 방법이다. 또한 엔티티, NBT 등 마인크래프트의 전반적인 메커니즘은 마인크래프트 위키에서 정리된 것을 쉽게 찾아볼 수 있으며 또한 대다수의 모드들은 오픈 소스이므로 이미 완성된 타 모드의 소스 코드를 참고하여 자신의 모드를 개발하는 데 응용할 수 있다. 단, 타 모드의 소스 코드를 참고·응용 목적으로만 쓰지 않고 그대로 복사·붙여넣기하여 사용하면 경우에 따라 라이선스 위반이 될 수 있으니 해당 모드의 라이선스를 참조해야 한다.자바 언어로 만들어진 마인크래프트에 모드를 만들기 위해서는 MCP (모드 코더 팩) 을 기반으로 한 Forge API를 이용해야 한다 (역시 자바 언어만 지원한다). non-forge[1] 스타일의 모드 개발은 추가 예정.
Forge로 모드를 만들려면 Java을 기본으로 다룰 줄 알아야 한다. 게다가 포지는 마인크래프트 게임 코드를 해독한 것인데, 새로운 버전이 나올때마다 포지 API의 함수나 구조가 자주 바뀌어서 버전에 따라 개발 과정이 달라지기도 한다. 따라서 최신 버전의 포지로 모드를 개발/업그레이드하려면 포지 API의 변화를 빠르게 잡아내고 익숙해질 필요가 있다.
만약 자바 언어가 서툴거나 프로그래밍에 낯설다면 아래에서 설명될 자동 개발 툴을 사용해 모드를 만들면 좀 더 쉽게 입문할 수 있다.
문서 작성은 개발 커뮤니티에서 주로 사용하는 튜토리얼 형식으로 작성하여 이용자들의 이해도를 높인다.
Forge 공식 개발 강좌문서
2. 개발 도구 설치
서로 엇갈리는 설명을 방지하기 위해 문서에서 다루는 툴은 eclipse로 통합한다. non-eclipse 개발도 있긴 하나 이 문서에서는 다루지 않는다.2.1. 자바 표준 개발환경 구축
JDK (자바 개발 도구)eclipse (IDE)
Intellij IDEA
이 툴은 작성시 기준으로 자바 10 구동이 불가능하므로 업데이트 전까지는 자바 8 사용을 권장한다.
먼저 해당 링크에서 JDK를 설치한다. 만약 윈도우 환경에서 개발할 경우 환경 변수를 추가해야한다. 이는 다음 절차를 참조.
제어판 → 시스템 밎 보안 → 시스템 → 고급 시스템 설정 → 환경 변수 → 사용자 변수에 추가 → JDK가 설치된 폴더의 bin을 참조시킨다.
그 다음, 명령 프롬프트를 열어서 java -version과 javac 명령어를 입력해 작동 여부를 확인한다.
마지막으로 이클립스를 설치한다. 이클립스는 설치 형식이 아니라 압축 파일에서 압축을 풀어서 사용하는 툴이니 원하는 폴더에 압축을 풀어 바로 사용하면 된다.
2.2. Forge API
Forge 다운로드포지 모드 개발환경의 설치 방식도 1.6에서 1.7로 넘어오면서 큰 변동이 일어났다. 여기서는 1.7 이상을 다룬다.
먼저 원하는 버전에 해당하는 포지 버전 중 아래의 파일을 선택해 다운받는다.
(가능한 한 권장(Recommended) 버전를 선택하자)
- 마인크래프트 1.7.10 까지는 Src 파일
- 마인크래프트 1.8 부터는 Mdk 파일
다운받은 파일을 원하는 폴더에 넣고 압축을 풀면 된다.
1.6과 다르게 1.7부터는 Gradlew(소문자 L이다) 설치기를 바탕으로 개발환경을 설치할 수 있기 때문에 모든 관리를 명령어로 해야 한다.
기본적으로 필요한 gradlew 명령어의 목록이다. 개발환경을 설치한 폴더에서 명령 프롬프트를 열고
gradlew.bat <명령어> 를 입력해 원하는 동작을 입력할 수 있다.
유닉스 계열(맥OS, 우분투 등) 운영체제를 사용중이라면 ./gradlew <명령어> 를 입력한다.
setupDecompWorkspace : 디컴파일된 개발환경 구축
eclipse : 구축된 개발환경을 이클립스의 개발방식으로 정돈
build : 모드를 컴파일한다
runClient : 마인크래프트에 모드를 적용해 실행시킨다
이들 중에서 setupDecompWorkspace 명령어로 개발환경 구축을 한다. 이때 인터넷이 느리거나 컴퓨터 CPU, 하드디스크 성능이 받쳐주지 못할 경우 설치시간이 영 좋지 않다(...) 컴퓨터도 열심히 설치를 하는데 인내심을 갖고 기다려주자.
위 설명은 1.12 기준이고, 1.13 이상이라면 genEclipseRuns 명령어를 사용해야 한다. 이처럼 나무위키 설명은 그 특성상 실시간으로 정보가 반영되기 힘드니, mdk 폴더에 동봉된 README.txt 설명서를 면밀히 확인하고 그대로 따라하는 게 좋다.
gradle을 이용한 워크스페이스인 만큼 만약 내장된 tasks(명령어들)들이 궁금하다면 gradlew tasks를 이용해서 확인 해볼 수 있다.
1.17은 자바 16, 1.18부터는 자바 17을 사용하므로 모드 개발을 위해서도 이에 맞는 JDK 버전이 필요하다. 1.17 이상 버전에 JDK 8을 사용하거나 1.16 이하 버전에 JDK 16 이상을 사용하면 오류가 발생한다.
2.3. Fabric API
Fabric 다운로드Fabric 개발 템플릿[2]
Fabric 위키[3]
fabric은 1.14 버전부터 최신버전까지 지원한다.
2.4. 개발 도구 통합과 마인크래프트 설치
개발 환경을 기반으로 정리 정돈을 해야 한다. 보통 이클립스를 사용하나 정 원한다면 src 폴더에서 직접 하드코딩을 한 후 gradlew의 명령어로 디버깅, 실행, 빌드 또한 가능하며 gradle 플러긴을 적용한다면 넷빈즈 또한 사용 가능하다. 사실상 gradlew와 호환된다면 거의 대부분의 IDE와 호환된다.일단 모드를 개발할 워크스페이스(폴더)를 만들어야 한다.
단, 폴더의 경로에 한글이 포함되어서는 안된다.
그 후 해당 폴더에서 shift+우클릭을 이용해 "여기서 명령 창 열기(w)"를 이용한 후 gradlew 관련 명령어를 실행하여 개발 환경을 설치하자.
보통 간단하게 gradlew에서 eclipse 명령어를 사용한다.
그 다음 이클립스를 켠다. 만약 이클립스를 처음 사용한다면 개발 공간을 할당하라는 창이 뜰 것이다. 만약 이미 공간을 할당했을 경우에는 File → Switch Workspace로 변경할 수 있다. 방금 forge API를 설치한 폴더에 eclipse를 할당한다.
최종적으로 이클립스에 초록색 벌레모양의 버튼(디버그)를 클릭한다. 마인크래프트가 정상적으로 로딩되며 포지의 설치가 확인될 경우 최종적으로 개발환경 구축이 끝난다. 디버그와 실행버튼의 차이는 디버그는 코드를 수정하면 즉시 반영된다.[4]
최종적으로 일반적으로 사용하는 마인크래프트를 설치한다. 당연히 필수는 아니며 설치를 권장할 뿐이다. 용도는 최종적인 모드 테스트와 버그를 찾는 용도. 직접 모드를 플레이하며 버그를 찾아다니면 분명히 예상하지 못한 버그들을 찾을 수 있다.
3. 패키지
소스코드를 만들기 전에 코드파일을 정리할 패키지 폴더를 먼저 만들자.자바에서 패키지 이름은 주로 도메인명.프로젝트이름 또는 닉네임.프로젝트이름 으로 정하는 관습이 있다.
예제: 자신의 모드 이름이 mymod 이고, 도메인이 namu.wiki 인 경우 패키지명
#!syntax java
wiki.namu.mymod
하지만 원한다면 기존 워크스페이스에서 만들어진 패키지를 벗어나서 작성할 수 있다. src폴더를 나가지 않으면 어떠한 패키지도 사용할 수 있다.
또한 패키지는 각종 모드 개발에 요구되는 파일들을 정리해 관리하는 역할도 담당한다.
다음 패키지는 기본적으로 정해진 것으로 미리 만들어두자.
참고로 패키지 이름에서 a.b 는 a 안에 b폴더가 있다는 의미이다.
assets : 모든 필요한 리소스들이 여기에 들어간다. 모드의 소스폴더와는 다른 resources라는 폴더에 생성한다.
assets.<모드 ID> : 아래와 같은 다양한 폴더들이 들어가는 상위폴더이다.
assets.<모드 ID>.textures : 아이템, 블록 등의 텍스쳐 파일을 넣는다.
assets.<모드 ID>.models : 웨이브 프론트를 이용해 3D 모델을 삽입할 때 사용할 obj 파일들이 들어간다.
assets.<모드 ID>.lang : 아이템, 블록 등의 이름을 정의할 lang파일들이 들어간다.
assets.<모드 ID>.blockstates : 블럭의 상태에 따른 3D 모델을 지정하는 파일을 넣는다.
assets.<모드 ID>.shaders : 셰이더 파일들이 들어간다.
assets.<모드 ID>.sounds : 각종 효과음으로 ogg 파일이 들어간다.
textures에 들어가는 하위 폴더들은 각각 문단에서 서술한다. 지금은 textures까지만 만들어둬도 무방하다.
패키지 이름은
1.13/1.14 이후
1.13 이후 assets 패키지 외에 data 패키지가 새로 생겼다. data 패키지 내에는 루트 테이블, 조합법, 아이템 태그, 발전 과제 등의 데이터를 저장하기 위한 파일들이 있다. data 패키지 역시 resource 폴더 내에 만들면 된다. 다음은 기본 data 패키지의 목록이다.
data.<모드 ID>.advancements : 발전 과제의 데이터를 저장하는 파일들을 모아놓은 패키지다.
data.<모드 ID>.loot_tables : 블럭, 몬스터의 드랍 아이템, 낚시를 할 때 낚이는 아이템, 폐광, 던전 등의 구조물 내에 생성되는
상자에서 발견되는 아이템 등을 설정하는 파일들을 모아놓은 패키지다.
data.<모드 ID>.recipes : 조합법 데이터를 모아놓은 패키지다.
data.<모드 ID>.tags : 여러 아이템이나 블럭을 하나로 묶은 태그에 대한 정보를 모아놓은 패키지다.
data.<모드 ID>.structures : 여러 생성 구조물의 정보를 모아놓은 패키지다.
4. 이름 등록 (GameRegistry)
원래 과거 포지에서는 LanguageRegistry 클래스를 이용해 각종 아이템과 블록들의 이름[5]을 등록했다. 그러나 소스코드 안에 이름을 변수로 저장해두면 코드가 난잡해지는 등의 문제가 있었다.그래서 최근에 이 클래스의 함수들이 대부분 Deprecated[6] 되었고, GameRegistry 가 등장하면서 아이템과 블록의 이름을 Lang 파일 안에 따로 모아서 기록할 수 있게 되었다.
그리고 1.12.2 버전 부터는GameRegistry가 Event형식으로 바뀌어 RegistryEvent.Register<T> 이벤트를 받아 등록을 할 수 있게 되었다.
먼저 프로젝트 폴더에서 assets.<모드 ID>.lang 패키지 안에 국가 코드.lang 의 이름으로 파일을 만든다. 예) ko_KR.lang
(한글로 작성된 내용이 있으면 UTF-8 인코딩으로 저장하자. 그렇지 않으면 게임 안에서 아이템 이름이 깨져서 보인다.)
한 개의 아이템 또는 블록 이름을 추가할 때 다음의 한 줄을 추가한다.
아이템일 때:
item.레지스트리 이름.name=표시할 이름
블록일 때:
tile.레지스트리 이름.name=표시할 이름
이 때 절대로 '=' 앞/뒤에 공백을 만들면 안된다. 파일 형식에 맞게 쓰지 않으면 컴퓨터는 오류를 뿜기 마련이다.
1.13/1.14 이후
1.13 이후 이름을 지정하는 파일의 형식이 json으로 바뀌었다. assets.<모드 ID>.lang 패키지 안에 기존과 같이 국가 코드로 파일명을 만드는 것까지는 같으나 확장자를 lang이 아닌 json으로 설정해야 한다. 따라서 당연히 json 문법으로 이름을 지정해야 한다.
예시
#!syntax json
{
"item.modnamu.namu_item": "나무 아이템",
"block.modnamu.namu_block": "나무 블럭"
}
파일명: ko_kr.json
5. 메인 클래스
포지 기반의 모드가 작동하는 양식을 보면 다음과 같다.
마인크래프트 게임이 부팅되면 게임엔진인 LWJGL 이 로드되고 각 클래스가 만들어져 추상화되는데,
이렇게 추상화된 클래스를 상속하고, 또 역으로 등록하여 엔진에 클래스를 추가한다.
즉, 상속받은 클래스 하나가 게임 속의 요소를 정의하는 하나의 객체가 된다. (객체지향 언어에서 흔하게 볼 수 있는 방식이다.)
모드 또한 위와 비슷한 방식으로 동작한다.
모드는 모드로더인 포지를 상속하는데, 포지에 모드를 등록하기 위해 어노테이션이 사용된다.
그런데 모드의 클래스 파일은 여러개가 존재할 수 있다. 하지만 그 중 메인 클래스 하나만 등록하면
모드 파일 전체가 포지에 등록되어 자신의 모드가 정상적으로 실행된다.
메인 클래스를 포지에 등록하는 법은 다음과 같다:
패키지 아래에 메인 클래스를 만들고 클래스 정의부분 위에 @Mod 어노테이션을 추가한다.
어노테이션의 () 괄호 안에는 모드의 아이디, 이름, 버전과 같은 속성값이 정의된다.
모드로더(포지) 는 모드를 읽어들일 때 메인 클래스의 이 어노테이션을 확인하기 때문에 이는 반드시 필요하며,
@Mod 어노테이션을 사용하려면 cpw.mods.fml.common.Mod 를 임포트해야 한다.[7]
#!syntax java
import cpw.mods.fml.common.Mod;
@Mod(modid=<모드 아이디>, name=<모드 이름>, version=<모드 버전>)
@Mod 어노테이션에 들어가는 속성값 중 modid[8] 는 반드시 입력해야 하며, 나머지는 필수 요소가 아니다. 이러한 속성값들은 각각 메인클래스에 정적 상수로 선언해 값을 정해놓고 불러와 쓰는 것이 일반적이다. (아래 종합 예재 코드 참고)
어노테이션 아래에는 클래스를 선언한다. 보통 클래스명은 자신의 모드 이름 또는 그 뒤에 Main이라는 단어를 붙여 쓰기도 한다.
그리고 Instance 어노테이션을 붙여 자신의 모드에 메인 클래스를 지정한다:
#!syntax java
@Instance(value = <모드 아이디>)
public static MyMod myMod;
마지막으로 이벤트 함수 위에 EventHandler 어노테이션을 붙여 포지에 등록함으로써 함수를 동작시킨다:
#!syntax java
@EventHandler
public void event(FML...Event event) {}
모든 이벤트 처리 함수는 파라미터에 FMLPreInitializationEvent, FMLInitializationEvent, FMLPostInitializationEvent 등
포지 이벤트 클래스가 한 개씩 있어야 한다.
EventHandler 어노테이션을 사용하려면 cpw.mods.fml.common.Mod.EventHandler 를 임포트해야 한다.
또한 각 파라미터의 객체가 되는 이벤트 클래스 또한 임포트해서 사용해야 한다.
예제
#!syntax java
import cpw.mods.fml.common.Mod.EventHandler;
import cpw.mods.fml.common.event.*;
//클래스 내부
@EventHandler
public void preInit(FMLPreInitializationEvent event) {}
@EventHandler
public void init(FMLInitializationEvent event) {}
@EventHandler
public void postInit(FMLPostInitializationEvent event) {}
이렇게 최종적으로 메인클래스 선언이 완료됐다. IDE에서 빌드 후 run해서 마인크래프트를 바로 실행해보면
메인 화면에서 모드 옵션에서 추가된 모드 목록 중에 자신의 모드가 보일 것이다.[9]
종합 예재 코드
#!syntax java
package wiki.namu.mymod;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.Mod.EventHandler;
import cpw.mods.fml.common.Mod.Instance;
import cpw.mods.fml.common.event.*;
@Mod(modid=NamuMain.MODID, name=NamuMain.NAME, version=NamuMain.VERSION)
public class NamuMain {
public static final String MODID = "modNamu";
public static final String NAME = "project Namu";
public static final String VERSION = "testing";
@Instance(value=NamuMain.MODID)
public static NamuMain namuobj;
@EventHandler
public void preInit(FMLPreInitializationEvent event) {
// 모드 로딩 중 init() 보다 먼저 호출되는 이벤트, 모드 설정을 불러오는 부분
}
@EventHandler
public void init(FMLInitializationEvent event) {
// 모드 로딩 중간에 호출되는 이벤트, 조합법과 광물을 추가하는 부분
}
@EventHandler
public void postInit(FMLPostInitializationEvent event) {
// 모드 로딩 중 init() 이후에 호출되는 이벤트, 다른 모드와 상호작용함으로써 호환성을 높이는 부분
}
}
마지막으로 클라이언트와 서버 프록시를 추가하여 게임상에 시각적인 렌더링을 하는 부분, 모드관련 쓰레드, 루프 동작 등을 하는 부분을 따로 작성할 수 있도록 클래스를 선언하는 것이 좋다. 프록시에 관한 부분은 아래에서 자세히 다룬다.
1.13/1.14 이후
1.13 이후 마인크래프트 코드가 크게 수정되면서 포지 코드 또한 대대적으로 수정되었다. 우선 메인 클래스를 포지에 등록할 때
@Mod
어노테이션을 사용하는 것 까지는 같으나, 이전과 다르게 모드 아이디만 입력할 수 있다. 이벤트 클래스 또한 그 명칭과 등록 방법이 변화하였다. 모드가 로드될 때 호출되는 이벤트는 net.minecraftforge.fml.event.lifecycle
패키지와 net.minecraftforge.fml.event.server
패키지 내에 정의되어 있다. 주요 이벤트 목록은 다음과 같다.FMLCommonSetupEvent : 이전의 preInit과 비슷한 역할을 한다. 아이템, 블럭 등의 여러 오브젝트를
등록하는 RegistryEvent.Register가 호출된 뒤에 호출되며, 이 후에 FMLClientSetupEvent와
FMLDedicatedServerSetupEvent가 호출된다. 이 때에는 모드 설정 파일을 불러오거나 월드 생성 옵션
등을 설정하는 등의 작업을 하면 좋다.
FMLClientSetupEvent : FMLCommonSetupEvent가 호출된 뒤에 호출된다. 이 이벤트가 호출되는
때에는 클라이언트 사이드에서 수행되는 여러 작업을 한다.
FMLDedicatedServerSetupEvent : FMLClientSetupEvent와 비슷하지만 서버 사이드에서 수행되는
작업을 수행한다는 점이 다르다.
InterModEnqueueEvent : FMLClientSetupEvent와 FMLDedicatedServerSetupEvent 뒤에 호출되는
이벤트로, 다른 모드와의 호환성을 위해 여러가지 정보를 보내는 일을 수행한다. InterModComms.sendTo
메소드를 사용해 정보를 보낼 수 있다.
InterModProcessEvent : InterModEnqueueEvent 뒤에 호출되며, 다른 모드들이 InterModEnqueueEvent
에서 보낸 정보를 받는 일을 수행한다. InterModComms.getMessages 메소드로 정보를 받을 수 있다.
위의 이벤트가 발생할 때 호출되는 이벤트 리스너는 위의 이벤트의 인스턴스를 파라미터로 받는 인스턴스 메소드를 정의하여 만들 수 있다.
예제
#!syntax java
import net.minecraftforge.fml.Logging;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
//클래스 내부
/* 디버깅을 위해서 모드의 각 클래스마다 org.apache.logging.log4j.Logger 인스턴스를 만들어놓는 것이
좋다. Logger 인스턴스는 org.apache.logging.log4j.LogManager.getLogger() 메소드를 호출하여 만들 수 있다. */
private static final Logger LOGGER = LogManager.getLogger();
private void setup(final FMLCommonSetupEvent event)
{
LOGGER.info(Logging.LOADING, "Setup method registered");
}
private void clientRegistries(final FMLClientSetupEvent event)
{
LOGGER.info(Logging.LOADING, "Client setup method registered");
}
각 이벤트의 인스턴스를 파라미터로 받기만 하면 메소드의 이름은 자유롭게 지을 수 있다. 이벤트 리스너를 만들었으면 이벤트 버스(Event bus)에 등록하면 된다. 이벤트 버스는 Mod 버스와 Forge 버스 두 종류가 있는데, Mod 버스에는 각 모드마다 서로 다른 특정한 일을 수행하는 이벤트가 등록되고(즉, 오브젝트 등록이나 여러가지 모드 로딩 작업 등은 Mod 버스에서 호출된다.), Forge 버스에는 그외의 모든 이벤트가 등록된다. 포지 API의 각 이벤트 클래스의 소스 파일 주석에 이벤트가 호출되는 버스가 언급되어 있으니 헷갈린다면 이를 참조하면 된다. 위에서 언급된 모든 이벤트는 모드 로딩 시에 호출되는 이벤트로, Mod 버스에 등록해야 한다. 이벤트 리스너 등록은
FMLJavaModLoadingContext.get().getModEventBus().addListener()
메소드로 할 수 있으며, 이 때 각 이벤트 인스턴스를 파라미터로 받아 아무것도 리턴하지 않는 함수 인터페이스인 Consumer의 구현체를 전달하면 된다. 즉, 위에서 정의한 이벤트 리스너를 람다식으로(java 8 기준) 넘겨주면 된다.#!syntax java
FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup);
FMLJavaModLoadingContext.get().getModEventBus().addListener(this::clientRegistries);
이벤트 리스너가 인스턴스 메소드이므로 이 작업은 메인 클래스의 생성자 내에서 해야 한다.
마지막으로, 반드시 해야하는 작업은 아니지만 메인 클래스의 인스턴스를 접근 제한자가 public인 정적 메소드로 저장하고, 메인 클래스의 인스턴스를 Forge 버스에 등록해주는 것이 좋다. 이렇게 하면 나중에 메인 클래스에 Forge 버스에 등록되어야 하는 이벤트 리스너를 만들 때, 리스너 위에
@SubscribeEvent
어노테이션만 붙이면 자동으로 등록되게 된다. 이는 MinecraftForge.EVENT_BUS.register()
메소드로 하며, 파라미터로 자기 자신의 인스턴스를 넘겨주면 된다.종합적인 코드는 다음과 같다.
#!syntax java
package wiki.namu.mymod;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.Logging;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@Mod(NamuMain.MODID)
public class NamuMain
{
public static NamuMain instance;
public static final String MODID = "modnamu";
private static final Logger LOGGER = LogManager.getLogger();
public NamuMain()
{
instance = this;
FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup);
FMLJavaModLoadingContext.get().getModEventBus().addListener(this::clientRegistries);
MinecraftForge.EVENT_BUS.register(this);
}
private void setup(final FMLCommonSetupEvent event)
{
LOGGER.info(Logging.LOADING, "Setup method registered");
}
private void clientRegistries(final FMLClientSetupEvent event)
{
LOGGER.info(Logging.LOADING, "Client setup method registered");
}
}
6. 클라이언트와 서버 사이드
마인크래프트는 까다로운 시스템을 가지고 있는데, 클라이언트(Client)와 서버(Server) 사이드이다.두 사이드는 서로 다른 기능을 하며, 코드가 분리된 채 따로 실행되어야 한다. 그렇지 않으면 예상치 못한 오류가 발생할 수도 있다!
마인크래프트상에서 일어날 법한 상황을 들어 두 사이드에 대해 설명해보면 다음과 같다:
만약 서버가 렉으로 멈추어도 유저들은 서버에 반영되지는 않겠지만 움직이거나 상자를 여는 등의 특정 동작을 수행할 수 있고, 반대의 경우로는 서버를 접속한 플레이어의 클라이언트가 렉이 걸린 시점에서 게임은 멈춘 것처럼 보이지만 서버에 있는 몬스터가 여전히 플레이어를 감지해서 때린다는 일련의 동작은 그대로 수행되어 데미지가 전달되고, 유저의 렉이 풀리면 그제서야 데미지를 받는 현상이 바로 위와 같은 게임상 사이드의 특성 때문이다.
이 개념은 우리가 일반적으로 아는 클라이언트와 서버의 정의와 거의 비슷한데, 클라이언트 사이드는 말 그대로 마인크래프트 게임 런처 또는 프로그램 자체를 뜻한다. 즉, 클라이언트 사이드에서 실행되는 것은 플레이어의 화면에 아이템을 렌더링하여 보여주는 코드, 특정 GUI의 동작 과정을 정의하는 코드, 키보드 입력을 인식하는 코드 등과 같이 플레이어 시점의 게임 화면에 영향을 주는 것이다.
서버 사이드에서 수행되는 코드란, 말 그대로 이 코드를 수행하는 위치가 서버에게 있음을 의미한다.
이러한 코드는 버킷과 같은 멀티플레이 서버 내부에서 수행되는 경우도 있지만, 클라이언트 사이드 내부에서 동작하는 경우도 있다.[10]
화로같이 특정 동작을 수행하는 블록 엔티티나 몬스터, 동물과 같은 엔티티의 움직임과 같은 동작 코드는 보통 이 쪽에서 많이 수행된다.
위에서 언급했다시피 서버 사이드 코드는 서버 쪽에서만 수행되기보다는 클라이언트 사이드와 같이 동작하는 경우가 많기에 클라이언트/서버가 같이 공동으로 수행할 코드는 공통 사이드로 묶은 뒤, 특정 사이드에서만 수행될 코드를 따로 편성해 방금 공통적으로 묶은 코드들에 상속하는 경우가 대다수이며 이를 코먼 사이드(Common Side)라고 부른다.
각 사이드의 코드에 대한 특성을 살펴보면 클라이언트 사이드에서 수행되는 코드들은 대부분 스피커나 키보드와 같이 플레이어의 컴퓨터 하드웨어를 이용하는 부분이 많고, 서버(코먼) 사이드에서 수행되는 코드는 아이템이 구워진다던가, 곡괭이로 광석을 캐내면 아이템이 나온다는 등의 실제 과정을 수행하는 부분이 많다.
6.1. @SideOnly
#!syntax java
@SideOnly(value = Side.CLIENT)
public void registerBlockIcons(IIconRegister p_149651_1_)
{
this.blockIcon = p_149651_1_.registerIcon(this.getTextureName());
// 마인크래프트 내 Block 클래스에서 실제 블록의 텍스쳐를 등록하는 메소드이며, 이 메소드는 클라이언트 사이드에서만 수행되어야 한다.
}
간혹 가다 위와 같이 @SideOnly 어노테이션이 붙은 필드나 메소드를 볼 수 있는데, 이것은
해당 필드나 메소드가 실행되는 시점이 주어진 사이드가 아니라면 컴파일하지 말라는 의미이다.
이제 위 예제를 해석해보자면, registerBlockIcons() 메소드가 클라이언트 쪽에서 실행된 것이 아니라면 메소드 실행이 중단될 것이다.
이처럼 @SideOnly 어노테이션을 쓰면 각각의 메소드나 필드를 사이드 별로 분리시킬 수 있다.
하지만 이 어노테이션은 사용이 권장되지 않고, 잘 쓰이지도 않는다.
그 이유는 만약 이 어노테이션을 잘못 사용해서 외부에서 어노테이션이 붙은 이 코드로 접근하면,
해당 코드가 의도하지 않은 사이드에서 실행되어 버려 오류의 원인이 되고, 코드가 난잡하게 꼬일 수 있기 때문이다.
따라서 위 어노테이션을 직접 사용하기보단 다음과 같은 방법으로 대체하자:
- world.isRemote() : 해당 월드의 사이드를 알아낸다
- FMLCommonHandler.getSide() : 이 메소드가 실행되는 시점의 사이드를 알아낸다.
위 메소드를 통해 현 시점의 (물리적) 사이드를 가려내어 코드가 실행될 것인지 말 것인지를 직접 구분지을 수 있다. 또는 아래 문서에 나와있는 프록시 기능을 사용할 수도 있다. 이 @SideOnly 어노테이션은 마인크래프트에서 이미 구현된 조상 클래스에서 해당 어노테이션이 붙어있는 메소드를 오버라이드[11]하고 싶을 때만 사용하는 것을 권장한다.
6.2. 프록시
위와 같은 마인크래프트의 서버/클라이언트 시스템을 획기적으로 조작할 수 있는 기법이다.거의 대부분의 모드 개발자들은 습관처럼 프록시를 나눠 사용하는데, 첫번째 이유는 이는 모드 개발자가 모델링 오브젝트 파일을 등록하여 자신만의 엔티티 모델을 렌더하거나, GUI, 파티클을 띄우는 등의 렌더링 작업을 담당하는 상당수의 코드가 클라이언트와 서버 사이드의 코드를 분류하여 사용하기 때문이다. 특히 위와 같은 @SideOnly 어노테이션이 붙은 코드같은 경우에는 자기 사이드가 아니면 컴파일 시 코드를 전부 무시해버리기 때문에, 자칫 제대로 분류하지 않으면 서버 사이드 측에서 컴파일 시에 무시된 클라이언트 사이드 측 코드가 불러와지는 등의 오류가 잦아질 수 있다. 이러한 현상을 예방하기 위해 프록시를 이용하여 미리 사이드 별로 불러올 함수를 나눠 모딩을 하는 것이 첫번째 이유이다.
두번째 이유는 클래스를 프록시에 따라 나눔으로써 코드의 번잡성을 줄이는 것인데,
만약 모드에 아이템이나 블록을 생성하여 등록하기 위해 GameRegistry 클래스의 메소드를 오버라이드한다면 등록하려는 아이템 수가 많지 않으면 그리 문제가 되지 않을 것이다. 하지만 등록하려는 아이템이 많아질 수록 메인 클래스의 코드 줄 수는 계속 늘어날 것이고 이는 프로젝트 유지 보수에 매우 불리하다.
모드 개발의 초반에는 보통 프록시 구분의 필요성이 부족하게 느껴지겠지만, 앞서 말했다시피 모드의 규모가 커질 수록 코드를 분리할 필요가 생길 것이므로 설령 지금은 쓸 일이 없더라도 클래스를 프록시로 미리 구분해 놓고 개발에 들어가는 것이 바람직하다.
프록시에 대한 실질적인 설명을 하자면 프록시를 통해 호출한 함수는 만약 호출한게 서버라면 코먼(Common) 프록시 클래스의 함수가 호출되고 만약 클라이언트라면 클라이언트(Client) 프록시 클래스의 함수가 자동으로 호출된다.
만약 Minecraft 코드 등에 @SideOnly 어노테이션이 있어서 서버와 클라이언트 둘다 이용하는 클래스 접근에 문제가 생긴다면 사용하는 것이 좋다.
단, 클라이언트(Client) 프록시 클래스에서 코먼(Common) 프록시 클래스의 함수를 오버라이딩하지 않고 프록시를 호출할 경우 당연히 부모 클래스인 코먼 프록시 클래스의 함수가 호출된다.
먼저 클래스 2개 중 하나는 Common이라는 키워드를, 하나는 Client라는 키워드를 포함하여 선언한다.
클래스의 이름은 그다지 중요하지 않으나 구별이 쉽도록 두 키워드를 사용해 짓도록 하자.
그런 다음 Client가 Common을 상속하도록 한다.
이제 메인 클래스에 @SidedProxy() 어노테이션을 호출하여 다음과 같은 프로토타입으로 Common의 객체를 만든다.
#!syntax java
@SidedProxy(clientSide="<Client의 위치>", serverSide="<Common의 위치>" )
public static CommonProxyClass objCommonProxy;
이것으로 프록시를 준비하는 일은 끝났다. 텅 빈 클래스 2개를 어디다 써먹는가 의문을 품을 수도 있지만 프록시는 사용될 확률이 매우 높은 기술이니 사용되는 일이 있을 경우 후술하도록 한다.
예제
#!syntax java
//메인 클래스 내부
@SidedProxy(clientSide="wiki.bluesky.main.ClientProxyClass", serverSide="wiki.bluesky.main.CommonProxyClass")
public static CommonProxyClass objCommonProxy;
클라이언트(Client) 프록시
#!syntax java
package wiki.namu.mymod;
public class ClientProxyClass extends CommonProxyClass {}
코먼(Common) 프록시
#!syntax java
package wiki.namu.mymod;
public class CommonProxyClass {}
1.13/1.14 이후
1.13 이후로
@SidedProxy
어노테이션이 사라졌는데, 그 이유는 같은 이벤트를 서로 다른 사이드에서 함께 부르던 이전과는 달리 사이드에 따라 부르는 이벤트가 구분되었기 때문이다. 따라서 굳이 프록시를 사용할 필요 없이 각 사이드에서 불리는 이벤트 리스너 내에서 각 사이드에서만 실행되어야 하는 코드를 실행하면 된다. 예를 들어 FMLCommonSetupEvent
에서는 양 사이드 모두에서 실행되어야 하는 코드를, FMLClientSetupEvent
에서는 클라이언트 사이드에서 실행되어야 하는 코드, 그리고 FMLDedicatedServerSetupEvent
에서는 서버 사이드에서 실행되어야 하는 코드를 실행하면 된다.굳이 프록시를 사용하고 싶으면
DistExecutor.runForDist()
메소드로 이전의 프록시를 흉내낼 수는 있다. 먼저 IProxy
인터페이스를 만든다. 그 다음, IProxy
인터페이스를 구현하는 ClientProxy
와 ServerProxy
클래스를 만든다. 그리고 모드 메인 클래스에 IProxy형 변수를 추가하고 다음의 코드를 실행하면 된다.#!syntax java
private static final IProxy proxy = DistExecutor.runForDist(() → ClientProxy::new, () → ServerProxy::new);
DistExecutor.runForDist()
메소드는 사이드에 따라서 서로 다른 코드를 실행하도록 하는 메소드로, 첫번째 파라미터에는 클라이언트 사이드에서 실행되어야 하는 코드를, 두번째 파라미터에는 서버 사이드에서 실행되어야 하는 코드를 넣어주면 된다. 이 때, 각 파라미터의 자료형은 Supplier<Supplier<T>>
이므로, 프록시 인스턴스를 리턴하는 함수를 리턴하는 함수를 넣어줘야 한다.7. 블록 만들기
대부분의 모더들이 처음으로 밟고 지나가는 절차이며 가장 마인크래프트의 기본적인 부분을 다루며 가장 눈으로 확인하기 쉬운 부분이기도 하다. 블록을 추가하는 일 또한 앞에서 서술했던 마인크래프트의 양식을 따라가야한다. 자바 문법에 벗어나지 않는 한에서 원하는 문법을 전부 적용 가능하다. 어나니머스 이너 클래스나 로컬 클래스, 이너클래스 등등 실험용 블록들은 이렇게 만들어 놓고 필요 없어지면 버리기도 한다. 데이터 관리를 위해서 정식으로 게임에 영향을 주게될 블록은 반드시 일반 클래스로 만들도록 하자.모든 블록은 Block클래스를 상속받아 그 안의 함수를 호출 혹은 재정의하여 마인크래프트에 추가된다. OOP의 매우좋은 예시 중 하나다. 상속된 자식은 부모의 위치에 들어가도 문제가 없어야 하며 부모의 함수를 똑같이 선언하면 그것을 오버라이딩이라 한다.
먼저 새로운 블록이 될 클래스를 생성/선언하여 Block을 상속받도록 한다.
부모 클래스가 되는 Block은 모든 블록의 프로토타입이며 게임에 직접 집어넣을 수 없다. 대신에 Block이라는 자료형을 담당하고 해당 자료형에 클래스들이 반드시 꼭 갖고 있어야하는 값들을 관리하는 용도의 클래스다. 예를 들어서 블록이 내는 빛, 블록의 딱딱함은 이러한 값들로 관리되며 자식 클래스에서 불편하게 새로 선언할 필요가 없다.
상속을 했으면 당연히 생성자를 만들어 내라고 오류를 뿜는다. Block의 생성자는 매개변수로 Material을 받는다. 이걸 받아서 부모의 객체에 넣는다
그리고나서 꼭 필요한 값들을 관리한다. 이 방법엔 2가지 방법이 있는데 Block의 생성자(또는 다른 함수)에서 관리하거나 메인 클래스에서 관리하는 방법이 있다.
이렇게 클래스가 완성되면 메인 클래스에서 객체를 만들어준다.
#!syntax java
//블록을 만들고 해당 블록을 어떠한 크리에이티브 탭에 추가할지 정하기.
//메인 클래스 안에서 값을 관리하기
//생성한 Block의 객체에 자식 객체를 넣었다. 어떻게 이런 구문이 정상작동 하는지는 후술.
public Block namuBlock = new BlockNamu(Material.glass).setCreativeTab(CreativeTabs.tabBlock);
//새로운 블록 클래스 안에서 관리하기
public BlockNamu(Material mat) {
super(mat);
setCreativeTab(CreativeTabs.tabBlock); //블록 클래스 안에서 부모의 setCreativeTabs를 호출한다.
}
마지막으로 블록을 등록한다. 블록을 등록하는 일은 Registry.Register<T> 이벤트를 받아 등록한다.
#!syntax java
import net.minecraft.*;
@SubscribeEvent
public static void registerBlock(RegistryEvent.Register<Block> e){
e.getRegistry().register(<블록 인스턴스>);
}
이 상태에서 마인크래프트를 실행하면 그냥 그대로 블록이 추가된다.
이 모델은 마인크래프트 엔진이 모델를 찾지 못 했을 때 임시로 렌더링을 하기 위해 만들어놓은 모델이며 모델를 추가로 설정하면 저절로 대처된다.
모델를 추가하기 위해선 먼저 패키지를 선언해야 한다. model 패키지 안에 block이라는 이름의 패키지를 넣어주고 그 안에 마인크래프트json형식의 모델을 넣어주면 된다. 여기서 주의할 점이 몇가지 있는데 바로 절대 패키지 이름이 대문자라면 작동하지 않는다. 이거 하나 실수하면 하루종일 고생한다. 또한 모델에 들어가는 이미지 크기는 반드시 16의 배수만을 사용한다. 즉, 16, 32, 64까지만 크기를 지원한다. 크기의 기준은 반드시 픽셀로 한다.
모델을 로드하려면 ModelRegistryEvent 이벤트 에서 모델을 설정해 주면 된다.
#!syntax java
ModelLoader.setCustomModelResourceLocation(Item.getItemFromBlock(<블록 인스턴스>), 0, new ModelResourceLocation(<블록 인스턴스>.getRegistryName(), "inventory"));
아마 이것을 복붙하는게 편할것이다.
나머지 블록 함수들과 변수들은 직접 레퍼런스를 읽으며 찾아 봐야 한다.[12] 대부분 mcp에 포함된 자바독이나 forge src에 포함된 자바독에도 블록 클래스에 관한 대부분의 설명은 나와있다. 참고로 한글은 없다.
자바에서 언제나 그래왔듯 마인크래프트도 오버라이딩을 위한 함수와 값을 받아내는 함수 그리고 오버라이딩을 위한 함수들로 구성되어있다. 함수에 정해진 용도란 의미가 무색하지만 사용되는 정석은 있다. 모든 함수와 필드들을 알 수는 없지만 알아두면 매우 편리한 몇가지 요소들을 나열한다.
다음은 블록 클래스 내부에서 사용 가능한 함수들의 일부분.
호출하여 값을 변경하는 set함수들. 리턴값이 this이기 때문에 만약 설정해야할 부분이 하나 이상일 경우에도 '.'연산자를 통해 연속으로 설정이 가능하다.
당연하지만 용도에 구애받지 않고 거의 대부분의 함수를 마음대로 오버라이딩 하여 자신만의 함수로 다시 재정의 해버릴 수 있다.
set함수들
Block setUnlocalizedName(String) : 블록의 지역화되지 않은 이름을 설정한다.
Block setCreativeTab(CreativeTabs) : 크리에이티브 모드의 탭에 해당 블록을 추가한다.
Block setBlockHardness(float) : 블록의 강도를 설정.
Block setResistance(float) : 블록의 폭발 저항력을 설정.
Block setHarvestLevel(String, int) : 문자열로 해당 도구의 클래스를, 정수로 채취 가능한 레벨을 설정한다.
Block setBlockUnbreakable() : 해당 블록을 기반암, 포탈과 같이 채굴할 수 없게 만든다. setBlockHardness(-1.0F) 함수를 호출하는것과 같은 기능을 한다.
오버라이딩하여 사용하는 함수들.
오버라이딩의 좋은 예제가 바로 마인크래프트에 있다. 멀리갈 것 없이 방금 만든 블록에 이것들을 오버라이딩 시켜 println() 파라미터들을 찍어보자.
void onBlockDestroyedByPlayer(World, BlockPos, IBlockState) : 플래이어에 의해서 블록이 파괴될 경우에 호출된다.
void onBlockActivated(World, BlockPos, IBlockState, EntityPlayer, EnumHand, EnumFacing, float, float, float) : 플래이어의 우클릭에 반응하여 호출한다.
void onEntityWalk(World, BlockPos, Entity) : 엔티티가 해당 블록 위에서 걸어다니고 있을 경우 호출된다.
void onBlockAdded(World, BlockPos, IBlockState) : 월드에 블록이 추가되었을 경우 호출된다.
void onBlockClicked(World, BlockPos, EntityPlayer) : 플래이어가 좌클릭으로 때릴 경우에 호출된다.
void onBlockExploded(World, BlockPos, Explosion) : 블록이 폭발로 인해 파괴될 경우에 호출된다.
void onEntityCollidedWithBlock(World, BlockPos, IBlockState, Entity) : 엔티티가 블록에 접촉한 경우 호출된다.
1.7 이전 버전의 마인크래프트에서는 좌표를 나타내기 위한 x, y, z 3개의 int값과 블록 상태를 나타내기 위한 int 값 데이터를 사용했으나, 최신 버전에서는 각각 BlockPos와 IBlockState로 대체되었으므로 구버전 모드의 소스코드를 참조하거나 자신의 모드를 업데이트할 경우 주의해야 한다.
값을 반환하는 함수들. 논리값이 리턴형인 함수들은 is, can이라는 접두사로 사용되니 주의할것. 일부 함수들은 접근 제한자로 호출이 자식 클래스에서만 가능한 경우가 있다.
추가 중.
블록의 렌더러를 재정의 하여 자신이 원하는 형태를 자유롭게 만들어내는 커스텀 렌더러(블록 엔티티 스페셜 렌더러)는
아래 블록 엔티티 부분에서 따로 서술한다.
1.13/1.14 이후
블록을 등록하기 위해
RegistryEvent.Register<T>
이벤트를 사용하는 것 까지는 같으나, 세부적인 부분에서 차이가 있다.우선 Registry 이벤트 리스너가 있는 클래스에
@Mod.EventBusSubscriber
어노테이션을 붙이고, 모드 아이디와 이벤트 버스를 넘겨줘야 한다. 만약 이벤트 리스너가 @Mod
어노테이션이 있는 클래스와 같은 곳에 있다면 모드 아이디는 생략해도 된다. 그렇기 때문에 Registry 이벤트 리스너 클래스를 따로 만들어도 되지만 메인 클래스 내의 정적 내부 클래스로 만드는 것이 편하다.Registry 이벤트는 Mod 버스에서 일어나는 일이기 때문에 bus 값을
Mod.EventBusSubscriber.Bus.MOD
로 넘겨준다.#!syntax java
package wiki.namu.mymod;
import net.minecraft.block.Block;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.RegistryEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.Logging;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.registries.ForgeRegistry;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@Mod(NamuMain.MODID)
public class NamuMain
{
public static NamuMain instance;
public static final String MODID = "modnamu";
private static final Logger LOGGER = LogManager.getLogger();
// 생략...
@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public static class RegistryEvents
{
private static final Logger LOGGER_REG = LogManager.getLogger();
@SubscribeEvent
public static void registerBlocks(final RegistryEvent.Register<Block> event)
{
LOGGER_REG.info(ForgeRegistry.REGISTRIES, "Blocks registered");
}
}
}
또한 특별한 기능이 없는 간단한 블록이라면
Block
클래스를 직접 인스턴스화해서 등록할 수도 있다.예를 들어 Registry 이름이 "namu_block"인 블록을 만들 때에는
#!syntax java
public static final Block NAMU_BLOCK = new Block(Block.Properties.create(Material.ROCK)).setRegistryName(MODID, "namu_block");
이런 식으로
Block
인스턴스를 생성할 수 있다. 이 때, setRegistryName()
메소드에 모드 아이디와 블록 이름을 함께 넘겨줘야 나중에 블록의 텍스처 등이 자동으로 등록되게 된다. 이렇게 하면 블록의 Registry 이름은 <모드 아이디>:<블록 이름>이 된다(위의 경우는 modnamu:namu_block).최신 버전의 포지에서는
Material
을 직접 생성자에 넘겨주던 이전과는 달리 Block.Properties
인스턴스를 넘겨주게 된다. 또한 이전버전에서 Block
클래스 내부에 있던 setter 메소드가 거의 대부분 Block.Properties
로 이동했다. 그리고 일부 블럭 특성은 getter 메소드를 오버라이드 하여 설정하도록 변경되었다. 예를 들어 블록 채집 도구와 채집 레벨은 각각 getHarvestTool()
과 getHarvestLevel()
메소드를 오버라이드 하여 설정한다.#!syntax java
public static final Block NAMU_BLOCK = new Block(Block.Properties.create(Material.ROCK)
.hardnessAndResistance(2.5f, 3.5f)){
@Override
public ToolType getHarvestTool(BlockState state)
{
return ToolType.PICKAXE;
}
@Override
public int getHarvestLevel(BlockState state)
{
return 1;
}
}.setRegistryName(MODID, "namu_block");
크리에이티브 탭을 설정하는 메소드도 사라졌는데, 이는 블록을 만들면 해당하는 아이템이 자동으로 추가되지 않고
BlockItem
인스턴스를 직접 등록해야 하기 때문이다. BlockItem
인스턴스를 생성할 때에는 생성자에 해당하는 블록의 인스턴스와 Item.Properties
인스턴스를 넣어주면 된다. 또한 블록 아이템의 Registry 이름은 블록과 동일하게 설정하는 것이 좋다.#!syntax java
public static final Item NAMU_BLOCK_ITEM = new BlockItem(NAMU_BLOCK, new Item.Properties().group(ItemGroup.BUILDING_BLOCKS))
.setRegistryName(NAMU_BLOCK.getRegistryName());
이전과의 차이점은 크리에이티브 탭을 설정하는 메소드가 역시
Item.Properties
내로 이동했다는 점과 CreativeTabs
이 ItemGroup
으로 바뀌었다는 점이다.이제 블록과 블록의 아이템을 포지에 등록하면 된다. 블록 아이템은
Item
클래스를 상속하기 때문에 블록과 다른 리스너에서 등록해야 한다. 아이템을 등록하는 리스너는 Registry.Register<T>
에서 T를 Item으로 바꾸기만 하면 된다. 또한, 여러개의 오브젝트를 등록할 때는 event.getRegistry().registerAll()
메소드를 사용할 수 있다.종합 예제 코드
#!syntax java
package wiki.namu.mymod;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.material.Material;
import net.minecraft.item.BlockItem;
import net.minecraft.item.Item;
import net.minecraft.item.ItemGroup;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.ToolType;
import net.minecraftforge.event.RegistryEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.Logging;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.registries.ForgeRegistry;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@Mod(NamuMain.MODID)
public class NamuMain
{
public static NamuMain instance;
public static final String MODID = "modnamu";
private static final Logger LOGGER = LogManager.getLogger();
public NamuMain()
{
instance = this;
FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup);
FMLJavaModLoadingContext.get().getModEventBus().addListener(this::clientRegistries);
MinecraftForge.EVENT_BUS.register(this);
}
private void setup(final FMLCommonSetupEvent event)
{
LOGGER.info(Logging.LOADING, "Setup method registered");
}
private void clientRegistries(final FMLClientSetupEvent event)
{
LOGGER.info(Logging.LOADING, "Client setup method registered");
}
@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public static class RegistryEvents
{
private static final Logger LOGGER_REG = LogManager.getLogger();
public static final Block NAMU_BLOCK = new Block(Block.Properties.create(Material.ROCK)
.hardnessAndResistance(2.5f, 3.5f)){
@Override
public ToolType getHarvestTool(BlockState state)
{
return ToolType.PICKAXE;
}
@Override
public int getHarvestLevel(BlockState state)
{
return 1;
}
}.setRegistryName(MODID, "namu_block");
public static final Item NAMU_BLOCK_ITEM = new BlockItem(NAMU_BLOCK, new Item.Properties().group(ItemGroup.BUILDING_BLOCKS))
.setRegistryName(NAMU_BLOCK.getRegistryName());
@SubscribeEvent
public static void registerItems(final RegistryEvent.Register<Item> event)
{
event.getRegistry().registerAll(
NAMU_BLOCK_ITEM
);
LOGGER_REG.info(ForgeRegistry.REGISTRIES, "Items registered");
}
@SubscribeEvent
public static void registerBlocks(final RegistryEvent.Register<Block> event)
{
event.getRegistry().registerAll(
NAMU_BLOCK
);
LOGGER_REG.info(ForgeRegistry.REGISTRIES, "Blocks registered");
}
}
}
이 상태에서 마인크래프트를 실행하면 건축 블록 탭에 추가한 블록이 있을 것이다.
하지만 위 사진과 같이 아무런 텍스처가 없고 서바이벌 모드에서 부숴도 블록이 드롭되지 않으며, 이름이 block.<모드 아이디>.<블록 이름>으로 되어 있을 것이다(위 예시의 경우 block.modnamu.namu_block). 따라서 블록에 텍스처와 모델링, 블록 상태, 블록의 루트 테이블을 추가해줘야 한다.
코드 내에서 모델이나 텍스처 등의 위치를 설정해줘야 했던 이전과는 달리 파일 위치와 이름 형식만 제대로 지킨다면 자동으로 로드된다.
먼저 블록의 텍스처 파일을 resources 폴더 내의 assets.<모드 아이디>.textures.block 패키지 내에 추가해준다. 이때, 파일 명은 블록의 이름과 동일해야 하며(위의 namu_block의 경우 텍스처 이름이 namu_block이어야 한다) 확장자는 .png여야 한다.
그다음 블록의 모델을 만들어줘야 한다. 모델 파일은 assets.<모드 아이디>.models.block 패키지 내에 추가해주며, 이름과 확장자는 <블록 이름>.json이어야 한다.
#!syntax json
{
"parent": "block/cube_all",
"textures": {
"all": "modnamu:block/namu_block"
}
}
파일명: assets/modnamu/models/block/namu_block.json
기본적으로 설정해야 할 프로퍼티는 parent와 texture다. parent는 블록의 모델의 기본이 될 모델을 지정하는 것으로 block/cube_all은 모든 정육면체 블록의 모델이다. textures는 블록의 각 면의 텍스처를 지정하는 것으로, 여기서는 모든 면이 같은 텍스처를 가지도록 all 프로퍼티로 지정했다. 텍스처명은 <모드 아이디>:block/<블록 이름>으로 작성해준다.
블록 모델에 대한 더 자세한 설명은 이곳 마인크래프트 영문 위키 Model 문서에서 찾을 수 있으며, 만약 바닐라 마인크래프트에 있는 블록과 비슷한 모델을 만들고 싶다면 %appdata%/.minecraft/versions/<마인크래프트 버전> 폴더 내의 jar 파일 내에서 assests/minecraft/models/block 폴더 내의 json 파일을 복사하여 적절히 수정하는 것이 편하다.
블록 모델 뿐만 아니라 블록의 아이템의 모델도 만들어줘야 한다. 이는 assets.<모드 아이디>.models.item 패키지 내에 방금 만든 파일과 같은 이름으로 만들어준다.
#!syntax json
{
"parent": "modnamu:block/namu_block"
}
파일명: assets/modnamu/models/item/namu_block.json
아이템의 모델은 간단히 블록의 모델을 부모로 설정해주면 된다. 이때 parent 프로퍼티의 값을 <모드 아이디>:block/<블록 이름>으로 설정한다.
그 다음 블록 상태를 지정하여 기본 상태에서 블록이 자신이 만든 모델을 가지도록 해야 한다. 먼저 assets.<모드 아이디>.blockstates 패키지 내에 <블록 이름>.json 파일을 추가해준다.
#!syntax json
{
"variants": {
"": {
"model": "modnamu:block/namu_block"
}
}
}
파일명: assets/modnamu/blockstates/namu_block.json
variants 프로퍼티는 블록이 가질 수 있는 상태들을 자식으로 가진다. 기본 블록 상태는 ""로 나타낸다. 각 블록 상태에 따로 모델을 지정해줄 수 있는데, 이렇게 하면 블록의 상태에 따라 서로 다른 모델을 가지게 된다.
예를 들어, 마인크래프트의 케이크는 먹은 횟수에 따라 다른 모델을 가지는데, 이는 variants 내에서 먹은 횟수를 나타내는 프로퍼티에 각각 다른 모델을 지정해줘서 구현할 수 있다.
각 블록 상태의 모델은 model 프로퍼티로 지정해주며, 모델의 이름은 <모드 아이디>:block/<블록 이름>으로 한다.
블록 상태에 대한 더 자세한 설명은 마인크래프트 영문 위키의 Model 문서 Block states 문단에서 찾을 수 있다.
만약 블록을 부쉈을 때 자기 자신을 드롭하게 하고 싶다면 블록에 루트 테이블을 추가해줘야 한다. data.<모드 아이디>.loot_tables.blocks 패키지 내에 <블록 이름>.json 파일을 만들어준다.
#!syntax json
{
"type": "minecraft:block",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "modnamu:namu_block"
}
],
"conditions": [
{
"condition": "minecraft:survives_explosion"
}
]
}
]
}
파일명: data/modnamu/loot_tables/blocks/namu_block.json
type 프로퍼티는 루트 테이블의 종류를 지정해주는 것으로, 여기서는 블록의 드랍 아이템을 설정하는 루트 테이블을 만들 것이므로 minecraft:block으로 설정한다.
pools 프로퍼티는 배열을 값으로 가지며, 배열에 여러 객체를 추가하여 상황에 따라 서로 다른 아이템을 드롭하도록 만들 수 있다. 여기서는 블록을 부쉈을 때 자기 자신이 드롭되도록만 할 것이기 때문에 객체는 하나만 만들면 된다.
rolls 프로퍼티는 pools 프로퍼티 내의 각 pool의 아이템이 드롭되는 개수를 나타내는 것으로, 아이템을 여러개 드롭하도록 만들고 싶다면 2 이상의 숫자를 지정할 수 있다. 또한 rolls 프로퍼티가 min 프로퍼티와 max 프로퍼티를 가지는 객체를 값으로 가지게 하여 일정 범위 내의 랜덤한 수만큼 드롭하게 할 수도 있다. 예를 들어
#!syntax json
"rolls": {
"min": 1,
"max": 6
}
이렇게 설정하면 아이템이 최소 1개에서 최대 6개까지 랜덤으로 드롭된다. entries 프로퍼티는 드롭될 아이템의 배열을 값으로 가지며, 프로퍼티 내의 객체에 weight 프로퍼티를 설정하여 각 아이템이 드롭될 확률을 정할 수 있다. 여기서는 블록이 자기 자신만 드롭하므로 entries 프로퍼티는 하나의 객체만을 가진다.
entries 프로퍼티 내의 각 객체는 기본적으로 type과 name 프로퍼티를 가진다. type은 드롭될 오브젝트의 종류를 지정하는 것으로, 아이템 뿐만 아니라 아이템 태그나 다른 루트 테이블을 지정할 수도 있다. 아이템을 드롭하게 하려면 minecraft:item을 값으로 설정하면 된다. name 프로퍼티는 드롭될 아이템을 나타내는 것으로 <모드 아이디>:<블록 이름>을 값으로 설정한다.
마지막으로 conditions 프로퍼티는 pools 프로퍼티 내의 각 객체가 실행될 조건을 지정하는 것이다. 여기서는 블록이 TNT나 크리퍼 등의 폭발로 부서졌을 때에도 드롭되도록 하기 위해 minecraft:survives_explosion을 condition으로 지정했다.
루트 테이블에 대한 더 자세한 설명은 이곳 영문 마인크래프트 위키의 Loot table 문서에서 확인할 수 있다. 만약 광물 블록 같이 도구에 붙은 인챈트에 따라 드롭되는 아이템의 종류와 개수가 달라지게 하고 싶은 등 블록 드롭을 다르게 설정하고 싶으면 역시 마인크래프트 jar 파일 내의 data/minecraft/loot_tables/blocks 폴더 내에서 비슷한 드롭 특성을 가진 블록의 루트 테이블을 복사하여 수정하는 것이 편하다.
마지막으로 assets.<모드 아이디>.lang 패키지 내에 파일을 추가하여 게임에서 보이는 블록의 이름을 설정한다. 기본적으로 영어 이름을 등록하는 en_us.json은 만들어주는 것이 좋으며, 그 외에 자신이 추가하고 싶은 언어를 <국가 코드>.json 파일로 만들어준다.
#!syntax json
{
"block.modnamu.namu_block": "Namu Block"
}
파일명: assets/modnamu/lang/en_us.json
각 블록의 이름을 설정하고 싶으면 block.<모드 아이디>.<블록 이름>을 이름으로 가지고 게임 내에서 표시되는 이름을 값으로 가지는 프로퍼티를 추가하면 된다.
모든 설정이 끝났다면 블록이 다음과 같이 보일 것이다.
블록과 블록 아이템에 텍스처가 생겼고, 적절한 도구를 사용하면 블록이 드롭되는 것을 확인할 수 있다. 이 예시에서는 블록이 돌 이상의 곡괭이로 캤을 때 드롭되도록 설정했다.
자신의 블록에 바닐라에 이미 있는 모델이 아닌 아예 새로운 모델을 만들어 적용하려 한다면 모델러 프로그램을 이용할 수 있다.
7.1. 나무 만들기
1.13/1.14 이후나무 원목과 나뭇잎은 각각
LogBlock
과 LeavesBlock
을 인스턴스화하여 만든다.#!syntax java
public static final Block NAMU_LOG = new LogBlock(MaterialColor.DIAMOND, Block.Properties.create(Material.WOOD, MaterialColor.CLAY).hardnessAndResistance(2.0F).sound(SoundType.WOOD))
.setRegistryName(MODID, "namu_log");
public static final Block NAMU_LEAVES = new LeavesBlock(Block.Properties.create(Material.LEAVES).hardnessAndResistance(0.2F).tickRandomly().sound(SoundType.PLANT))
.setRegistryName(MODID, "namu_leaves");
이 블록들도 필요한 경우 아이템을 추가하도록 한다. LogBlock
의 생성자에는 MaterialColor
와 Block.Properties
가 들어가는데, 첫번째 MaterialColor
는 원목의 단면의 색이고, Block.Properties
내부의 MaterialColor
는 원목 껍질의 색이다. 그 외의 다른 설정들은 마인크래프트 바닐라의 원목과 잎의 설정과 같다.그리고 나무 원목과 잎의 텍스처와 모델링, 블록 스테이트를 설정하도록 한다. 원목의 경우 다른 블록들과 다르게 옆면과 단면의 텍스처가 다르므로 따로 넣어줘야 한다. 옆면 텍스처 이름은 블록 이름, 단면 텍스처 이름은 <블록 이름>_top으로 하는 것이 좋다. 모델링과 블록 스테이트는 실제 마인크래프트 내의 파일을 참조해서 적당히 수정해주도록 한다.
#!syntax json
{
"parent": "block/leaves",
"textures": {
"all": "modnamu:block/namu_leaves"
}
}
파일명 : assets/modnamu/models/block/namu_leaves.json
#!syntax json
{
"parent": "block/cube_column",
"textures": {
"end": "modnamu:block/namu_log_top",
"side": "modnamu:block/namu_log"
}
}
파일명 : assets/modnamu/models/block/namu_log.json
#!syntax json
{
"variants": {
"": {
"model": "modnamu:block/namu_leaves"
}
}
}
파일명 : assets/modnamu/blockstates/namu_leaves.json
#!syntax java
{
"variants": {
"axis=y": {
"model": "modnamu:block/namu_log"
},
"axis=z": {
"model": "modnamu:block/namu_log",
"x": 90
},
"axis=x": {
"model": "modnamu:block/namu_log",
"x": 90,
"y": 90
}
}
}
파일명 : assets/modnamu/blockstates/namu_log.json
그리고 나무 원목과 잎으로 구성되는 나무 객체 그 자체를 나타내는
Tree
클래스의 상속체를 구현해야 한다. Tree
클래스에서 구현해야 하는 메소드는 다음 하나다.#!syntax java
AbstractTreeFeature<NoFeatureConfig> getTreeFeature(Random random)
위 메소드가 반환하는 AbstractTreeFeature<NoFeatureConfig>
클래스는 나무가 어떻게 생성될 지를 결정한다. 자신이 만들 Tree
클래스의 상속체는 자신이 정의한 AbstractTreeFeature<NoFeatureConfig>
의 상속체를 반환하면 된다. AbstractTreeFeature<NoFeatureConfig>
클래스에서 구현해야 하는 메소드는 다음 하나다.#!syntax java
boolean place(Set<BlockPos> changedBlocks, IWorldGenerationReader worldIn, Random rand, BlockPos position, MutableBoundingBox box)
위 메소드는 파라미터로 받은 세계
worldIn
내에서의 위치 position
에서 나무가 생성될 수 있는지 확인하고 생성될 수 있으면 나무를 생성한 뒤 true를 반환하고 아니면 false를 반환한다. 위 메소드의 다양한 구현 방식은 net.minecraft.world.gen.feature
패키지 내의 AbstractTreeFeature<NoFeatureConfig>
의 상속체들에 정의되어 있으니 이를 참고하여 적당히 수정하면 된다. 가장 간단한 경우로 추가적인 가지가 없고 큰 나무가 자라지 않으며 위쪽 네 칸에 잎이 생기는 최소 5칸 최대 8칸 높이의 나무를 만드는 클래스는 다음과 같다.#!syntax java
package wiki.namu.mymod.world.gen;
import com.mojang.datafixers.Dynamic;
import net.minecraft.block.BlockState;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MutableBoundingBox;
import net.minecraft.world.gen.IWorldGenerationReader;
import net.minecraft.world.gen.feature.AbstractTreeFeature;
import net.minecraft.world.gen.feature.NoFeatureConfig;
import net.minecraftforge.common.IPlantable;
import wiki.namu.mymod.NamuMain;
import java.util.Random;
import java.util.Set;
import java.util.function.Function;
public class NamuTreeFeature extends AbstractTreeFeature<NoFeatureConfig>
{
private static final BlockState LOG = NamuMain.RegistryEvents.NAMU_LOG.getDefaultState();
private static final BlockState LEAF = NamuMain.RegistryEvents.NAMU_LEAVES.getDefaultState();
private static final int minimumHeight = 5;
public NamuTreeFeature(Function<Dynamic<?>, ? extends NoFeatureConfig> deserializeFunction, boolean doBlockNofityOnPlace)
{
super(deserializeFunction, doBlockNofityOnPlace);
}
@Override
public boolean place(Set<BlockPos> changedBlocks, IWorldGenerationReader worldIn, Random rand, BlockPos position, MutableBoundingBox box)
{
int height = minimumHeight + rand.nextInt(4);
boolean place = true;
// 생성 위치가 블록 설치 가능 범위 내에 있는지 확인
if(position.getY() >= 1 && position.getY() + height + 1 <= worldIn.getMaxHeight())
{
// 주변에 충분한 공간이 있는지 확인
for(int y = position.getY(); y <= position.getY() + height + 1; y++)
{
int margin = 1;
if(y == position.getY())
margin = 0;
if(y >= position.getY() + height + 1 - 2)
margin = 2;
BlockPos.MutableBlockPos marginPos = new BlockPos.MutableBlockPos();
for(int x = position.getX() - margin; x <= position.getX() + margin; x++)
for(int z = position.getZ() - margin; z <= position.getZ() + margin; z++)
if(!func_214587_a(worldIn, marginPos.setPos(x, y, z))) // 해당 위치의 블록이 공기거나 나무가 자랄 수 있는 블록인지 확인
place = false;
}
if(!place)
return false;
else if(isSoil(worldIn, position.down(), getSapling()) && position.getY() + height + 1 < worldIn.getMaxHeight())
{
// 나무 생성
setDirtAt(worldIn, position.down(), position);
int[] widths = {1, 2, 2, 3};
for(int y = position.getY() + height - 3; y <= position.getY() + height; y++)
{
int relY = y - position.getY() - height;
int width = widths[Math.abs(relY)];
for(int x = position.getX() - width; x <= position.getX() + width; x++)
{
int relX = x - position.getX();
for(int z = position.getZ() - width; z <= position.getZ() + width; z++)
{
int relZ = z - position.getZ();
if(Math.abs(relX) != width || Math.abs(relZ) != width || (rand.nextInt(2) != 0 && relY != 0))
{
BlockPos placePos = new BlockPos(x, y, z);
if(isAirOrLeaves(worldIn, placePos))
setLogState(changedBlocks, worldIn, placePos, LEAF, box);
}
}
}
}
for(int y = 0; y < height; y++)
{
if(isAirOrLeaves(worldIn, position.up(y)))
setLogState(changedBlocks, worldIn, position.up(y), LOG, box);
}
return true;
}
else
return false;
}
else
return false;
}
}
생성자는 AbstractTreeFeature<NoFeatureConfig>
클래스의 생성자 그대로 만들었다. 위 코드의 첫번째 부분은 나무가 세계 높이 제한 내에 생성될 수 있으며, 나무가 생길 기둥 주위 1칸, 위에서 2칸에서는 기둥 주위 2칸이 비어있거나 나무가 생성될 수 있는 블록(잎, 흙, 잔디, 원목, 묘목, 덩굴)인지 확인한다. 나무가 생성될 수 있는 공간이 충분하다면 나무 생성 위치 아래 칸이 흙인지 확인하고 나무를 생성한다. 나무 생성 부분의 코드를 수정하면 다양한 모양의 나무를 만들 수 있다. place()
메소드에서 사용하면 유용한 메소드는 다음이 있다.static boolean func_214587_a(IWorldGenerationBaseReader p_214587_0_, BlockPos p_214587_1_) : 세계 내의 해당 위치에서
나무가 생성될 수 있는지 반환하는 AbstractTreeFeature<NoFeatureConfig>의 정적 메소드다.
static boolean isSoil(IWorldGenerationBaseReader reader, BlockPos pos, net.minecraftforge.common.IPlantable sapling) :
세계 내의 해당 위치의 블록이 흙인지 확인하여 반환하는 AbstractTreeFeature<NoFeatureConfig>의 정적 메소드다.
IPlantable 인스턴스는 AbstractTreeFeature<NoFeatureConfig> 클래스의 getSapling() 인스턴스 메소드의 리턴값으로 주면 된다.
void setDirtAt(IWorldGenerationReader reader, BlockPos pos, BlockPos origin) : 해당 위치의 잔디를 흙으로 바꾼다.
이 때, pos는 바꿀 흙 블록의 위치, origin은 나무 기둥이 시작되는 위치다.
static boolean isAirOrLeaves(IWorldGenerationBaseReader p_214572_0_, BlockPos p_214572_1_) : 세계 내의 해당 위치의 블록이
공기 또는 잎인지 확인한다.
void setLogState(Set<BlockPos> changedBlocks, IWorldWriter worldIn, BlockPos p_208520_3_, BlockState p_208520_4_, MutableBoundingBox p_208520_5_) :
해당 위치의 블록을 자신이 생성할 블록으로 바꾼다.
나머지 파라미터는 place 메소드의 파라미터를 그대로 넣으면 되고, BlockPos는 생성할 블록의 위치,
BlockState는 생성할 블록의 BlockState를 넣으면 된다.
AbstractTreeFeature<NoFeatureConfig>
를 구현했으면 Tree
의 구현체가 위 클래스의 인스턴스를 반환하도록 하면 된다.#!syntax java
package wiki.namu.mymod.block.tree;
import net.minecraft.block.trees.Tree;
import net.minecraft.world.gen.feature.AbstractTreeFeature;
import net.minecraft.world.gen.feature.NoFeatureConfig;
import wiki.namu.mymod.world.gen.NamuTreeFeature;
import java.util.Random;
public class NamuTree extends Tree
{
@Override
public AbstractTreeFeature<NoFeatureConfig> getTreeFeature(Random random)
{
return new NamuTreeFeature(NoFeatureConfig::deserialize, true);
}
}
doBlockNotifyOnPlace
파라미터는 기본적으로 true로 하도록 하자.묘목은
SaplingBlock
을 인스턴스화하여 만들면 된다. 무슨 이유에서인지 SaplingBlock
클래스의 생성자가 protected
접근 한정자로 선언되어 있어 모드 내에서 접근이 불가능하므로 SaplingBlock
을 상속해서 생성자에 접근이 가능하도록 해야 한다.#!syntax java
package wiki.namu.mymod.block;
import net.minecraft.block.SaplingBlock;
import net.minecraft.block.trees.Tree;
public class ModSaplingBlock extends SaplingBlock
{
public ModSaplingBlock(Tree tree, Properties properties)
{
super(tree, properties);
}
}
위 클래스의 첫번째 파라미터에는 자신이 만든 Tree
클래스의 상속체의 인스턴스를 넣어주면 된다.#!syntax java
public static final Block NAMU_SAPLING = new ModSaplingBlock(new NamuTree(), Block.Properties.create(Material.PLANTS).doesNotBlockMovement().tickRandomly().hardnessAndResistance(0).sound(SoundType.PLANT))
.setRegistryName(MODID, "namu_sapling");
묘목을 만들었으면 아까 만든
AbstractTreeFeature<NoFeatureConfig>
상속체의 생성자 내에서 setSapling()
메소드로 해당 클래스가 사용할 묘목을 자신이 만든 묘목으로 설정해줘야 한다.#!syntax java
public class NamuTreeFeature extends AbstractTreeFeature<NoFeatureConfig>
{
//...
public NamuTreeFeature(Function<Dynamic<?>, ? extends NoFeatureConfig> deserializeFunction, boolean doBlockNofityOnPlace)
{
super(deserializeFunction, doBlockNofityOnPlace);
this.setSapling((IPlantable)NamuMain.RegistryEvents.NAMU_SAPLING);
}
//...
}
그리고 다른 블록과 마찬가지로 마인크래프트 바닐라의 묘목의 설정 파일을 참조해서 텍스처와 모델링, 블록스테이트를 설정해주면 된다.#!syntax json
{
"parent": "block/cross",
"textures": {
"cross": "modnamu:block/namu_sapling"
}
}
파일명 : assets/modnamu/models/block/namu_sapling.json
#!syntax json
{
"variants": {
"": {
"model": "modnamu:block/namu_sapling"
}
}
}
파일명 : assets/modnamu/blockstates/namu_sapling.json
이대로만 해도 묘목으로 나무를 자라게 하는 데에는 문제가 없지만 나무가 생성된 뒤 잎이 자동으로 사라진다. 이는 마인크래프트가 모드에서 만든 잎과 원목을 잎과 원목으로 인식하지 못하기 때문이다. 따라서 마인크래프트의
leaves
와 logs
아이템 태그에 자신이 만든 블록을 추가해줘야 한다. 아이템 태그를 추가하기 위해서는 data 폴더 내의 <모드 아이디>.tags.blocks(블록 태그), <모드 아이디>.tags.items(아이템 태그) 폴더 내에 <태그 명>.json 파일을 만들어주면 된다. 여기서는 바닐라 마인크래프트의 태그에 새 블록을 추가할 것이므로 마인크래프트의 모드 아이디인 minecraft.tags.blocks 패키지 내에 파일을 추가한다.#!syntax json
{
"replace": false,
"values": [
"modnamu:namu_leaves"
]
}
파일명 : data/minecraft/tags/blocks/leaves.json
#!syntax json
{
"replace": false,
"values": [
"modnamu:namu_log"
]
}
파일명 : data/minecraft/tags/blocks/logs.json
여기서
replace
프로퍼티는 기존의 태그 목록을 덮어쓰기 할 것인지 설정한다. 기존 태그에 덧붙일 것이므로 false로 설정한다. values
프로퍼티 내에는 해당 태그에 추가할 블록의 Registry 이름을 넣어주면 된다. 아이템 태그에 대한 더 자세한 설명은 여기 마인크래프트 영문 위키 내의 Tag 문서에서 확인할 수 있다.7.2. 유체 블록 만들기
마인크래프트 모드 내에서의 유체는 실제로 월드 내에서 블록 형태로 구현되지 않은 부분인 Fluid 클래스를 상속받아 만들어진 객체가 유체의 특성(밀도, 온도 등)을 관한 데이터를 저장하고, Block 클래스를 상속받은 유체 블록 클래스가 실제 월드 내에서 블록 형태로 구현할 때 유체에 관한 정보를 담고있는 Fluid 객체를 인자로 받아 블록 인스턴스를 생성하는 형태로 되어있다.즉, 예를 들면 '나무즙'이라는 이름의 액체를 Fluid 클래스를 상속받아 FluidNamuJuice란 클래스를 만들어 생성시킨 다음, 유체 블록 클래스를 통해 나무즙의 유체 블록을 구현시키지 않으면 나무즙은 그저 탱크나 양동이와 같이 액체를 담는 용기에서 존재할 수 밖에 없는 데이터 쪼가리로밖에 존재하지 않는다. [13]
따라서 자신이 만든 유체를 월드에 놓고싶다면, 아래의 예제와 같이 Fluid 클래스를 상속받아 유체의 개념과 특성을 정의한 객체를 만든 다음, 그 객체를 유체 블록 클래스의 생성자 안에 담아 블록 형태로 구현시켜야한다.
#!syntax java
package wiki.bluesky.common.fluids;
/**
* 이 클래스는 Fluid 클래스를 상속받아 만들어졌으며, 유체의 개념과 특성을 정의할 수 있습니다.
* 이 클래스의 변수들은 기본적으로 마인크래프트 물의 값을 Default로 갖습니다.
* @Author Estiv
*/
import net.minecraftforge.fluids.Fluid; // 나무즙 클래스의 부모 클래스가 될 Fluid 클래스
public class FluidNamuJuice extends Fluid {
FluidNamuJuice() {
super("NamuJuice"); // Fluid 클래스의 생성자의 인자는 'String fluidName' 이며, 액체의 이름(UnlocalizedName)을 결정짓는다.
}
setDensity(1000); // 액체의 밀도를 정의하는 함수, 마인크래프트 물의 밀도는 1000의 값을 갖는다. 단위는 킬로그램/세제곱미터이다. 음수를 입력할 경우 공기보다 가벼운 것으로 처리된다.
setTemperature(295); // 유체의 온도를 정의하는 함수, 기본 295의 값을 갖는다. 켈빈 온도이므로 설정하기 원하는 온도에 273을 더해야 한다. (예: 액체 온도가 100℃면 373K 이다.)
setLuminosity(0); // 유체가 얼마나 빛을 발산하는가를 정의하는 함수, 기본 0의 값을 가지며 0~15사이의 값이 유효하다.
setViscosity(1000); // 유체의 점성을 정의하는 함수, 확산 속도에 영향을 주며 기본 1000의 값을 갖는다.
setGaseous(false); // 유체가 액체인지, 기체인지를 결정짓는 함수. true를 입력할 경우 유체는 위로 솟는 기체의 특성을 갖게되며, 기본 false의 값을 갖는다.
}
#!syntax java
package wiki.namu.mymod;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fluids.Fluid;
import net.minecraftforge.fluids.FluidRegistry;
public class CommonProxyClass {
public void init(FMLInitializationEvent event) {
Fluid namuJuice = new FluidNamuJuice();
FluidRegistry.registerFluid(namuJuice); // 자신이 만든 새로운 Fluid를 등록시키는 메소드이다.
}
}
[14]다음과 같이 자신만의 유체 클래스를 만들어서 자신이 만들 유체의 특성을 정해놓을 수 있다. Fluid 클래스를 상속받은지라 Fluid 클래스의 메소드를 사용할 수 있는데, 주로 쓰이는 메소드는 다음과 같다.
메소드 | 역할 |
setDensity(int density) | 유체의 밀도의 값을 변환시킨 후, 변환된 값을 리턴한다. |
setTemperature(int temperature) | 유체의 온도의 값을 변환시킨 후, 변환된 값을 리턴한다. |
setLuminosity(int luminosity) | 유체의 발광도의 값을 변환시킨 후, 변환된 값을 리턴한다. |
setViscosity(int viscosity) | 유체의 점성의 값을 변환시킨 후, 변환된 값을 리턴한다. |
setGaseous(boolean b) | 유체가 액체인지 기체인지를 정하고, 유체의 상태를 리턴한다. |
setIcons(IIcon still, IIcon flowing) | 유체가 멈춰있을 때와 흐를 때의 텍스쳐를 지정한다. |
다음은 정의한 유체에 대응하는 블록을 만들어야 만든 유체를 월드 내에 설치할 수 있다고 위에서 언급하였으니, 새로운 블록을 정의해보자.
액체 블록을 정의할 때 상속받는 클래스는 보통 두 가지로 나눌 수 있는데, BlockFluidBase 클래스와 BlockFluidClassic 클래스로 나눌 수 있다. 전자는 유체의 기본적 특성은 따오지만 자신만의 새로운 특성을 추가하거나 재정의하고 싶을 때 많이 상속되고, 후자는 바닐라 마인크래프트 유체의 특성을 따르는 일반적인 액체 블록을 정의할 때 많이 상속된다. 이 문서 내에선 후자의 경우를 다룬다.
#!syntax java
package wiki.namu.mymod.common.blocks;
/**
* 이 블록은 현재 지정된 Texture가 없습니다.
* @Author Estiv
*/
import net.minecraftforge.fluids.BlockFluidClassic; // 유체 블록 클래스의 부모가 되는 클래스.
import net.minecraftforge.fluids.Fluid;
import net.minecraft.block.material.Material;
public class BlockFluidNamuJuice extends BlockFluidClassic {
BlockFluidNamuJuice(Fluid fluid) {
super(fluid, Material.water); // 유체 블록의 생성자. 요구 인자는 Fluid, Material 인스턴스이며, Material은 블록의 기본 재질을 결정한다.
}
}
#!syntax java
package wiki.namu.mymod;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.registry.GameRegistry;
import net.minecraftforge.fluids.Fluid;
import net.minecraftforge.fluids.FluidRegistry; // 유체는 별도의 레지스트리를 가지므로 import로 따로 호출해준다.
import net.minecraft.block.Block;
import wiki.namu.mymod.common.blocks.BlockFluidNamuJuice;
public class CommonProxyClass {
public void init(FMLInitializationEvent event) {
Fluid namuJuice = new FluidNamuJuice();
FluidRegistry.registerFluid(namuJuice); // 나무즙 유체 추가
Block blockNamuJuice = new BlockFluidNamuJuice(namuJuice);
GameRegistry.registerBlock(blockNamuJuice, "blockNamuJuice"); // 나무즙 유체 *블록* 추가
}
}
위 문단을 참고하여 블록 클래스를 만들고 그것을 프록시 클래스에 등록하였다. 이 때, 이 클래스에선 유체 블록을 다루는 클래스기에 생성자에 인자가 더 늘어났는데 바로 Fluid 인스턴스이다. 상속한 클래스가 생성하는 인스턴스는 유체 블록의 개념을 상정하고 만들어진터라 반드시 대응되는 유체가 존재해야 하기에 인자에 Fluid 인스턴스가 있어야 하는 것을 유념해야한다.
7.2.1. 유체 컨테이너 만들기
밑의 블록 엔티티 추가 항목의 컨테이너 추가 항목 참고.7.3. 블록 엔티티 추가
블록 엔티티(타일 엔티티)는 엔티티처럼 정보를 담을수 있는 동시에 블록의 성질 또한 가지고 있는 블록이다. 이것을 이용해 화로, 명령 블록과 같은 기능적 블록을 만들 수 있다. 하지만, 일반 블록보다 만들기 대체적으로 어렵고, 렉이 발생할 수 있어 일반 블록으로 구현할 수 있을 시 최대한 일반 블록으로 만드는 것이 좋다. 특히 돌이나 흙처럼 월드에 대량으로 자연 생성되는 블록에는 블록 엔티티를 추가하지 말아야 한다.7.3.1. 블록 구현
7.3.2. 블록 엔티티 구현
7.3.3. 컨테이너 추가
7.3.4. GUI 추가
7.3.5. 텍스처 추가
7.3.6. 전력(포지 에너지) 추가
8. 크리에이티브 탭 만들기
말 그대로 크리에이티브 모드에서 사용할 크리에이티브 탭이다. 일명 아이템 그룹이라고도 읽는다.블록과 마찬가지로 크리에이티브 탭을 관리하는 클래스를 상속해 등록해 버리면 끝이다. 하지만 방식이 블록보다 단순해 클래스를 만드는 행위는 코딩의 낭비를 야기시킬 수 있다. 그렇기 때문에 다른 특별한 것을 넣기는 게 아니라면 새로운 클래스를 작성하는 방법보다 그냥 익명 클래스로 만들어버리면 끝이다. 심지어 등록할 필요도 없다.
CreativeTabs 클래스의 객체를 만들어 버리고 getTabIconItem 함수를 재정의해 아이콘만 만들어주면 끝이다.
예제
#!syntax java
CreativeTabs tabs = new CreativeTabs("tab Namu") {
public Item getTabIconItem() {
return Items.bed;
//아이콘을 블록으로 하고 싶다면 Item.getItemFromBlock 함수를 통해 블록을 아이템으로 변경 후 리턴하면 된다.
}
};
만약 아이템을 등록하고 싶지만 그럴 수 없는 경우[15] 아이템을 임시로 아이콘 전용으로 만들어 등록하면 된다.
1.13/1.14 이후
몇가지 변경점이 있다. 우선
CreativeTabs
클래스가 ItemGroup
클래스로 변경되었다. 또한 getTabIconItem()
메소드가 createIcon()
으로 바뀌었으며, Item
대신 ItemStack
을 리턴한다.ItemStack
클래스는 여러 개 겹쳐 있는 아이템을 나타내기 위한 클래스다. ItemStack
클래스는 IItemProvider
인터페이스의 구현체를 생성자로 받는다. IItemProvider
는 asItem()
메소드를 정의하고 있으며, 이 메소드는 구현하고 있는 오브젝트에 해당하는 아이템을 리턴하도록 되어 있다. Item
과 Block
클래스가 IItemProvider
를 구현하고 있으므로 이 둘을 생성자에 넘겨줄 수 있다.단,
Block
인스턴스를 넘겨줄 때에는 주의할 점이 있는데, 해당 블록의 아이템 형태가 구현되어 있어야 한다는 것이다. ItemStack의 생성자는 넘겨받은 IItemProvider
구현체의 asItem()
메소드를 호출하여 아이템 인스턴스를 받아 필드에 저장한다. 그런데 블록의 경우 블록의 아이템 형태가 정의되어 있지 않으면 기본적으로 공기를 반환하게 된다. 따라서 크리에이티브 탭의 아이콘이 정상적으로 나타나지 않는다.#!syntax java
package wiki.namu.mymod.item;
import net.minecraft.item.ItemGroup;
import net.minecraft.item.ItemStack;
import wiki.namu.mymod.NamuMain;
public final class NamuItemGroups
{
private NamuItemGroups() {}
public static final ItemGroup NAMU = new ItemGroup("namu") {
@Override
public ItemStack createIcon() { return new ItemStack(NamuMain.RegistryEvents.NAMU_BLOCK); }
};
}
이런식으로 크리에이티브 탭을 따로 모아놓은 클래스를 만드는 것이 좋다. 이렇게 하면 따로 등록할 필요 없이 필요할 때
ItemGroup
의 인스턴스를 가져다 쓰면 된다.또한 게임 내에서 표시되는 크리에이티브 탭의 이름을 lang 패키지 내의 파일에 등록해줘야 한다. 이 때 프로퍼티의 이름은 itemGroup.<탭 이름>이며, 탭 이름은
ItemGroup
생성자에 넣은 문자열이다.#!syntax json
{
"block.modnamu.namu_block": "Namu Block",
"itemGroup.namu": "Namu"
}
파일명: assets/modnamu/lang/en_us.json
이후 아이템을 등록할 때,
ItemGroup
으로 자신이 만든 탭을 전달하면 자동으로 게임 내에서 보여지게 된다.#!syntax java
public static final Item NAMU_BLOCK_ITEM = new BlockItem(NAMU_BLOCK, new Item.Properties().group(NamuItemGroups.NAMU))
.setRegistryName(NAMU_BLOCK.getRegistryName());
실행시 다음과 같이 크리에이티브 탭이 만들어진 것을 볼 수 있다.
9. 아이템 만들기
아이템을 만드는것은 블럭과 매우 비슷하지만, 그것보다 더욱 간단하다.일단 블럭이 아니라 아이템을 상속받아 클래스를 만든다.
#!syntax java
public class EGTItem extends Item{
public EGTItem(String name) {
this.setUnlocalizedName(name);
this.setRegistryName(name);
}
}
그다음 블럭과 같이 필드를만들고 등록하고 모델을 적용시킨다.
참고로 모델은 item하위패키지에 넣는다.
10. 인챈트 만들기
1.13/1.14 이후인챈트를 만들기 위해서는
Enchantment
클래스를 상속하는 클래스를 만들면 된다. Enchantment
클래스의 기본 생성자는 다음과 같다.#!syntax java
protected Enchantment(Enchantment.Rarity rarityIn, EnchantmentType typeIn, EquipmentSlotType[] slots)
생성자의 각 파라미터를 설명하면 다음과 같다.Enchantment.Rarity : 인챈트의 레어도로, 인챈트가 등장할 확률을 정해준다. COMMON, UNCOMMON, RARE, VERY_RARE의 네가지 레어도가 있다.
EnchantmentType : 마법 부여대에서 해당 인챈트를 할 수 있는 아이템의 종류를 정해준다.
EquipmentSlotType : 해당 인챈트를 할 수 있는 장비를 장착할 수 있는 슬롯을 정해준다. 총 6개의 슬롯 타입이 있다.
- MAINHAND : 오른손에 들 수 있는 아이템이다.
- OFFHAND : 왼손에 들 수 있는 아이템이다. 거의 쓸 일은 없다고 보면 된다.
- FEET : 발에 낄 수 있는 아이템이다.
- LEGS : 다리에 낄 수 있는 아이템이다.
- CHEST : 몸통에 낄 수 있는 아이템이다.
- HEAD : 머리에 낄 수 있는 아이템이다.
Enchantment
를 상속한 클래스의 생성자에서는 super
키워드로 위의 파라미터를 부모 클래스에 넘겨줘야 한다. 또한 Enchantment
클래스의 여러 메소드를 적절히 오버라이드 하여 세부적인 설정을 할 수 있다. 다음은 대표적인 메소드 목록이다.int getMinLevel() : 인챈트의 최소 레벨을 리턴한다. 기본값은 1이다. 웬만해서는 오버라이드하여 바꿀 필요가 없다.
int getMaxLevel() : 인챈트의 최대 레벨을 리턴한다. 기본값은 1이다.
int getMinEnchantability(int enchantmentLevel) : 각 인챈트 레벨마다 필요한 최소 플레이어 레벨을 리턴한다. 기본값은 레벨*10+1이다.
int func_223551_b(int p_223551_1_) : 이름이 이상하게 되어있지만 getMaxEnchantability라고 보면 된다. 각 인챈트 레벨마다 필요한
최대 플레이어 레벨을 리턴한다. 기본값은 최소값+5 이다.
int calcModifierDamage(int level, DamageSource source) : 해당 인챈트가 적용된 장비를 장착하고 있을 때, 인챈트 레벨에 따른
플레이어가 받는 데미지 감소량을 리턴한다. DamageSource 종류에 따라 서로 다른 값을 리턴할 수 있으며, 데미지 감소량은 리턴값*4%이다.
float calcDamageByCreature(int level, CreatureAttribute creatureType) : 해당 인챈트가 적용된 장비로 다른 몹이나 플레이어를 때렸을
때 들어가는 추가 데미지를 리턴한다.
boolean canApplyTogether(Enchantment ench) : 파라미터로 전달받은 인챈트를 해당 인챈트와 함께 적용할 수 있는지 리턴한다. 예를
들어 활의 무한 인챈트와 수선 인챈트는 함께 적용할 수 없으므로 무한 인챈트의 구현 클래스인 InfinityEnchantment의 해당 메소드는 수선
인챈트를 파라미터로 받았을 때 false를 리턴한다.
boolean canApply(ItemStack stack) : 파라미터로 받은 아이템 스택 내의 아이템에 해당 인챈트를 모루에서 적용할 수 있는지 리턴한다.
예를 들어 날카로움은 마법 부여대에서 도끼에 적용할 수 없지만 모루에서는 적용할 수 있다. 따라서 해당 인챈트 클래스의 canApply 메소드는
AxeItem을 상속하는 아이템을 가지는 ItemStack 인스턴스를 받았을 때 true를 리턴한다.
void onEntityDamaged(LivingEntity user, Entity target, int level) : 해당 인챈트가 적용된 아이템을 사용한 user가 target에게
데미지를 입혔을 때 자동으로 호출되는 메소드다. 주의할 점은, 활로 화살을 쏘아 맞춘 경우 활로 '직접' 때린 것이 아니기 때문에
이 메소드가 호출되지 않는다.
void onUserHurt(LivingEntity user, Entity attacker, int level) : 해당 인챈트가 적용된 아이템을 장비한 user가 attacker에게
데미지를 받았을 때 자동으로 호출되는 메소드다.
boolean isTreasureEnchantment() : 해당 인챈트가 수선, 차가운 걸음과 같이 마법 부여대에서 뜨지 않는 보물 마법부여인지 리턴한다.
boolean isCurse() : 해당 인챈트가 귀속 저주, 소실 저주와 같이 마법 부여대에서 뜨지 않는 저주 마법부여인지 리턴한다.
예를 들어 마법 부여대에서 검에 적용할 수 있고, 모루에서 도끼에 적용할 수 있으며 타격 시 상대에게 구속 효과를 거는 인챈트는 다음과 같이 구현할 수 있다.
#!syntax java
package wiki.namu.mymod.enchantment;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.enchantment.EnchantmentType;
import net.minecraft.enchantment.Enchantments;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.inventory.EquipmentSlotType;
import net.minecraft.item.AxeItem;
import net.minecraft.item.ItemStack;
import net.minecraft.potion.EffectInstance;
import net.minecraft.potion.Effects;
public class FrostEnchantment extends Enchantment
{
public FrostEnchantment(Rarity rarityIn, EquipmentSlotType... slots)
{
super(rarityIn, EnchantmentType.WEAPON, slots);
}
@Override
public int getMaxLevel()
{
return 3;
}
@Override
public int getMinEnchantability(int enchantmentLevel)
{
return 5 + 7 * enchantmentLevel;
}
@Override
public int func_223551_b(int enchantmentLevel) //getMaxEnchantability 기본값은 min + 5
{
return getMinEnchantability(enchantmentLevel) + 10;
}
@Override
public boolean canApplyTogether(Enchantment ench) //발화 인챈트와 함께 걸 수 없다
{
return super.canApplyTogether(ench) && ench != Enchantments.FIRE_ASPECT;
}
@Override
public boolean canApply(ItemStack stack)
{
return stack.getItem() instanceof AxeItem;
}
@Override
public void onEntityDamaged(LivingEntity user, Entity target, int level)
{
if(target instanceof LivingEntity)
{
((LivingEntity)target).addPotionEffect(
new EffectInstance(Effects.SLOWNESS, 20 * 2 * level, level - 1)
);
}
super.onEntityDamaged(user, target, level);
}
}
LivingEntity
는 몹이나 플레이어 등 모든 살아있는 엔티티의 최상위 클래스다. 위 인챈트의 효과는 때린 상대가 살아있는 엔티티인 경우에만 적용되므로 target이 LivingEntity의 인스턴스인 경우에만 효과를 적용하도록 코드를 작성한다.target에 상태 효과를 적용하기 위해서는 target의 자료형을
LivingEntity
로 변환하고 addPotionEffect()
메소드를 호출하면 된다. 이 메소드는 상태 효과의 종류와 지속시간, 레벨을 저장하는 EffectInstance
의 인스턴스를 받는다. EffectInstance
의 생성자에는 상태 효과의 종류, 단위가 틱인 지속시간(1초는 20틱이다), 그리고 효과의 레벨-1을 순서대로 전달하면 된다. 상태 효과는 Effects
클래스에 있는 기본 마인크래프트 효과나 자신이 정의한 Effect
인스턴스를 전달하면 된다.활에 적용되는 인챈트의 경우 특별한 효과를 적용하기 위해서는 위와 다른 방법을 사용해야 하는데, 그 이유는 위에서 언급했듯이 활로 화살을 쏘아 엔티티를 맞춘 경우
onEntityDamaged
메소드가 호출되지 않기 때문이다.이 경우에는 인챈트 클래스 내에
EntityJoinWorldEvent
의 이벤트 리스너를 만들어서 구현할 수 있다. 리스너를 자동으로 등록하기 위해서는 인챈트 클래스에 @Mod.EventBusSubscriber
어노테이션을 붙이고, 리스너에 @SubscribeEvent
어노테이션을 붙여주면 된다. 이 때, 인챈트 클래스는 보통 메인 클래스 내에 있지 않으므로 @Mod.EventBusSubscriber
에 modid
값을 넘겨줘야 하며, EntityJoinWorldEvent
이벤트는 Forge 버스에서 호출되므로 bus
값을 Mod.EventBusSubscriber.Bus.FORGE
로 넘겨줘야 한다.다음은 맞췄을 때 상대에게 시듦 효과를 거는 활에 적용할 수 있는 인챈트의 구현 코드다.
#!syntax java
package wiki.namu.mymod.enchantment;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.enchantment.EnchantmentType;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.projectile.AbstractArrowEntity;
import net.minecraft.entity.projectile.ArrowEntity;
import net.minecraft.inventory.EquipmentSlotType;
import net.minecraft.item.BowItem;
import net.minecraft.item.CrossbowItem;
import net.minecraft.item.ItemStack;
import net.minecraft.potion.EffectInstance;
import net.minecraft.potion.Effects;
import net.minecraftforge.event.entity.EntityJoinWorldEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import wiki.namu.mymod.NamuMain;
@Mod.EventBusSubscriber(modid = NamuMain.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public class WithersArrowEnchantment extends Enchantment
{
public WithersArrowEnchantment(Rarity rarityIn, EquipmentSlotType... slots)
{
super(rarityIn, EnchantmentType.BOW, slots);
}
@Override
public Rarity getRarity()
{
return Rarity.VERY_RARE;
}
@Override
public int getMaxLevel()
{
return 2;
}
@Override
public int getMinEnchantability(int enchantmentLevel)
{
return enchantmentLevel * 30;
}
@Override
public int func_223551_b(int enchantmentLevel) //getMaxEnchantability
{
return getMinEnchantability(enchantmentLevel) + 50;
}
@Override
public boolean isTreasureEnchantment()
{
return true;
}
@SubscribeEvent
public static void onEntityJoinWorld(EntityJoinWorldEvent event)
{
if(!(event.getEntity() instanceof ArrowEntity))
return;
ArrowEntity arrow = (ArrowEntity)event.getEntity();
if(!(arrow.getShooter() instanceof LivingEntity))
return;
LivingEntity shooter = (LivingEntity)arrow.getShooter();
ItemStack heldOnMainHand = shooter.getHeldItemMainhand();
ItemStack heldOnOffHand = shooter.getHeldItemOffhand();
int levelOnMainHand = EnchantmentHelper.getEnchantmentLevel(NamuMain.RegistryEvents.WITHERS_ARROW, heldOnMainHand);
int levelOnOffHand = EnchantmentHelper.getEnchantmentLevel(NamuMain.RegistryEvents.WITHERS_ARROW, heldOnOffHand);
int level = 0;
if(heldOnMainHand.getItem() instanceof CrossbowItem)
return;
if(heldOnMainHand.getItem() instanceof BowItem)
{
if(levelOnMainHand <= 0)
return;
level = levelOnMainHand;
}
else if(heldOnOffHand.getItem() instanceof BowItem)
{
if(levelOnOffHand <= 0)
return;
level = levelOnOffHand;
}
if(level <= 0)
return;
arrow.addEffect(new EffectInstance(Effects.WITHER, 20 * (4 * level + 1), 1));
arrow.pickupStatus = AbstractArrowEntity.PickupStatus.DISALLOWED;
}
}
EntityJoinWorldEvent
는 세계 내에 엔티티가 생성되었을 때 호출된다. 날아가는 화살 또한 엔티티이므로 플레이어가 화살을 쏘았을 때도 이 이벤트가 호출된다. 다만, 화살 외에도 모든 엔티티가 생성되었을 때 이 이벤트가 호출되므로, EntityJoinWorldEvent
의 getEntity()
메소드로 생성된 엔티티의 종류를 확인할 필요가 있다. 화살 엔티티는 ArrowEntity
의 인스턴스이므로 이벤트의 엔티티가 ArrowEntity
의 인스턴스인지 확인하면 된다.만약 엔티티가 화살이면
ArrowEntity
로 형변환을 하여 화살을 발사한 대상을 getShooter()
메소드로 받을 수 있다. 활을 쏠 수 있는 대상은 플레이어를 비롯해 모든 살아있는 대상이므로 shooter가 LivingEntity
의 인스턴스인지 확인하면 된다.그 다음, shooter를
LivingEntity
로 형변환하여 getHeldItemMainhand()
와 getHeldItemOffhand()
메소드로 각각 오른손과 왼손에 들고 있는 아이템 스택을 받을 수 있다. 이렇게 받은 아이템 스택에 getItem()
메소를 사용하여 해당 스택의 아이템을 받을 수 있으며, EnchantmentHelper.getEnchantmentLevel()
메소드에 인챈트 인스턴스와 함께 전달하여 아이템에 붙은 해당 인챈트의 레벨을 확인할 수 있다. 인챈트 인스턴스는 다른 곳에 정적 상수로 저장해놔야 전달할 수 있다. 여기서는 NamuMain.RegistryEvents
클래스 내에 WITHERS_ARROW
라는 이름의 상수로 저장해두었다.그리고 조금 복잡한 코드를 사용하여 화살이 인챈트가 붙은 활에서 발사되었는지 확인해야 한다. 단순히 두 손의 아이템 중 하나가 활인지 확인하고 인챈트 레벨을 확인하게 되면 왼손에 인챈트가 붙은 활을 들고 오른손에 쇠뇌나 일반 활을 들어 인챈트 붙은 활의 내구도는 소모하지 않으면서 인챈트 효과는 적용되는 꼼수를 쓸 수 있기 때문이다.
기본적으로 마인크래프트 내에서 아이템을 사용할 때 오른손에 있는 아이템이 우선적으로 사용된다. 따라서 양 손에 활이나 쇠뇌 등 화살을 발사할 수 있는 아이템이 있으면 오른손에 든 아이템에서 우선적으로 발사된다. 이를 이용해 화살이 인챈트 붙은 활에서 발사되었는지 확인할 수 있다.
먼저
level
이라는 이름의 int형 변수를 선언하여 0으로 초기화한다. 오른손에 든 아이템의 종류를 확인하여 쇠뇌인 경우 쇠뇌에는 해당 인챈트가 붙지 않으므로 리턴해주면 된다. 만약 오른손의 아이템이 활인 경우 오른손의 아이템에 적용된 인챈트 레벨이 0 이하라면 인챈트가 붙지 않았다는 의미이므로 리턴해주면 되고, 그렇지 않은 경우 level 변수에 해당 인챈트 레벨을 저장한다.만약 오른손의 아이템이 쇠뇌도 활도 아니라면 화살이 왼손의 아이템에서 발사되었다는 의미이므로 왼손의 아이템의 종류를 확인하면 된다. 왼손의 아이템이 활인 경우 마찬가지로 해당 아이템에 붙은 인챈트 레벨을 확인하여 레벨이 0 이하면 리턴하고 아니면 level 변수에 인챈트 레벨을 저장한다.
이 모든 과정이 끝났다면 level 변수에는 화살을 발사한 활에 붙은 인챈트의 레벨이 저장되어 있을 것이다. 그러면 앞서 받아두었던 화살 엔티티에
addEffect()
메소드로 효과를 추가할 수 있다. 이 메소드 역시 EffectInstance
의 인스턴스를 받는다.이렇게 하면 한가지 문제가 생기는데, 인챈트가 붙은 활로 발사한 화살이 물약이 묻은 화살로 바뀌고 이를 다시 주울 수 있다는 것이다. 즉, 위 인챈트가 붙은 활을 이용해 화살을 시듦 효과를 지닌 화살로 변환할 수 있는 것이다. 이를 방지하기 위해 화살 엔티티의
pickupStatus
필드의 값을 AbstractArrowEntity.PickupStatus.DISALLOWED
로 설정하여 화살을 줍지 못하게 만든다.이렇게 인챈트 클래스를 만들었으면 인스턴스화하여 어딘가에 저장해두는 것이 좋다.
#!syntax java
public static final Enchantment FROST = new FrostEnchantment(Enchantment.Rarity.RARE, EquipmentSlotType.MAINHAND)
.setRegistryName(MODID, "frost");
public static final Enchantment WITHERS_ARROW = new WithersArrowEnchantment(Enchantment.Rarity.VERY_RARE, EquipmentSlotType.MAINHAND)
.setRegistryName(MODID, "withers_arrow");
인챈트 역시 블록이나 아이템의 인스턴스를 만들때와 마찬가지로 Registry 이름을 지정해줘야 한다. 이렇게 만든 인챈트 인스턴스를 RegistryEvent.Register<Enchantment>
이벤트의 리스너에서 블록이나 아이템을 등록하는 방법과 같은 방법으로 등록해주면 된다.#!syntax java
@SubscribeEvent
public static void registerEnchantments(final RegistryEvent.Register<Enchantment> event)
{
event.getRegistry().registerAll(
FROST,
WITHERS_ARROW
);
LOGGER_REG.info(ForgeRegistry.REGISTRIES, "Enchantments registered");
}
마지막으로 인챈트 역시 lang 패키지 내의 파일에 게임 상에서 표시될 이름을 지정해야 한다. 이 때, 각 프로퍼티의 이름은 enchantment.<모드 아이디>.<인챈트 이름>으로 정한다.#!syntax json
{
...
"enchantment.modnamu.frost": "Frost",
"enchantment.modnamu.withers_arrow": "Wither's Arrow"
}
파일 이름: assets/modnamu/lang/en_us.json
11. 상태이상(포션 효과 추가)
12. 키 바인딩(조작키 추가)
마인크래프트 내 입력이 감지될 때
InputEvent.KeyInputEvent
가 발생한다. 따라서 이 이벤트의 리스너를 등록하면 키보드 입력을 받을 수 있다. 하지만 해당 이벤트의 메소드로는 입력된 키를 받기만 할 수 있고 키가 처음 눌렸는지 등의 세부적인 정보는 알 수 없다. 또한 위 이벤트의 리스너만 등록하게되면 키 바인딩을 게임 내에서 수정하기 불편해지며, 설정한 키가 수행하는 역할을 게임 내에서 알기도 어렵다. 따라서 일반적으로 특정 키의 역할과 카테고리를 저장하는 KeyBinding
클래스의 인스턴스를 등록하여 사용한다.KeyBinding
클래스의 생성자는 다음과 같다.#!syntax java
public KeyBinding(String description, int keyCode, String category)
각 파라미터는 다음의 역할을 한다.
description : 키가 눌렸을 때 어떤 일을 수행하는지 알려준다.
다른 모드와의 중복을 피하기 위해 <모드 아이디>.key.<이름>으로 짓는 것이 좋다.
이름은 다른 오브젝트의 이름과 마찬가지로 소문자와 언더바를 사용한다.
keyCode : 해당하는 키의 코드이다. org.lwjgl.glfw 패키지 내의 GLFW 클래스에 정의되어 있는 것을 사용한다.
category : 해당 키 바인딩의 카테고리이다.
같은 카테고리를 가진 키 바인딩은 마인크래프트 설정 내에서 같은 카테고리에 묶이게 된다.
key.categories.<모드 아이디>.<세부 카테고리>로 짓는 것이 좋다.
예를 들어 J키를 눌렀을 때 현재 플레이어의 장소를 스폰 포인트로 지정하는 역할을 하게 하고 싶다면 다음의 인스턴스를 만들면 된다.
#!syntax java
import net.minecraft.client.settings.KeyBinding;
import static wiki.namu.mymod.NamuMain.MODID;
import static org.lwjgl.glfw.GLFW.*;
//...
public static final KeyBinding SET_SPAWN = new KeyBinding(MODID + ".key.set_spawn", GLFW_KEY_J, "key.categories." + MODID);
또다른 키 바인딩을 같은 카테고리로 묶고 싶다면 category
로 같은 문자열을 넘겨주면 된다.#!syntax java
public static final KeyBinding KILL = new KeyBinding(MODID + ".key.kill", GLFW_KEY_K, "key.categories." + MODID);
키 바인딩을 만들었으면 레지스트리에 등록해줘야 한다. 키 바인딩이나 입력 등은 클라이언트 사이드에서만 행해져야 하는 작업이기 때문에 메인 클래스 내의
FMLClientSetupEvent
리스너 내에서 등록해야 한다. 등록은 ClientRegistry.registerKeyBinding()
메소드로 한다.#!syntax java
private void clientRegistries(final FMLClientSetupEvent event)
{
LOGGER.info(Logging.LOADING, "Client setup method registered");
ClientRegistry.registerKeyBinding(SET_SPAWN);
ClientRegistry.registerKeyBinding(KILL);
}
그 다음
InputEvent.KeyInputEvent
의 리스너에서 키 바인딩의 인스턴스의 isKeyDown()
또는 isPressed()
메소드를 호출하여 해당 키가 눌렸는지 확인할 수 있다. 둘의 차이점은 isKeyDown()
은 키가 눌려있는 상태면 항상 true
를 반환하지만, isPressed()
는 키가 눌린 시점에만 true
를 반환한다.주의할 점은 위 이벤트는 클라이언트 사이드에서만 처리되어야 하기 때문에 리스너에
@OnlyIn(Dist.CLIENT)
어노테이션을 붙여야 한다. 그러므로 명령어 실행 등 서버 사이드에서 실행되는 작업을 직접 리스너 내에서 할 수 없다. 서버 사이드에서 실행되어야 하는 작업은 서버에 패킷을 보내는 방식으로 처리해야 한다.#!syntax java
package wiki.namu.mymod.input;
import net.minecraft.client.settings.KeyBinding;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.client.event.InputEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;
import wiki.namu.mymod.NamuMain;
import static wiki.namu.mymod.input.NamuKeyBindng.SET_SPAWN;
import static wiki.namu.mymod.input.NamuKeyBindng.KILL;
@Mod.EventBusSubscriber(modid = NamuMain.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public class NamuKeyInputHandler
{
public static final Logger LOGGER = LogManager.getLogger();
public static final Marker INPUT = MarkerManager.getMarker("INPUT");
@OnlyIn(Dist.CLIENT)
@SubscribeEvent(receiveCanceled = true)
public static void onKeyInput(InputEvent.KeyInputEvent event)
{
if(SET_SPAWN.isPressed())
{
LOGGER.debug(INPUT, "Key binding = " + SET_SPAWN.getKeyDescription());
// 해당하는 키가 눌렸을 때 수행할 작업을 여기에 작성
// 명령어 수행 등 서버 사이드에서 수행되어야 할 작업은 서버에 패킷을 보내서 수행
}
if(KILL.isPressed())
{
LOGGER.debug(INPUT, "Key binding = " + KILL.getKeyDescription());
}
}
}
마지막으로 키 바인딩의 카테고리와 설명이 게임 상에서 보이는 이름을 lang 패키지 내에 설정해줘야 한다. 이 때, 프로퍼티의 이름은
KeyBinding
클래스의 생성자에 넣어준 문자열 그대로 하면 된다.#!syntax json
{
"key.categories.modnamu": "Namu Mod",
"modnamu.key.set_spawn": "Set Spawnpoint",
"modnamu.key.kill": "Suicide"
}
파일 이름 : assets/modnamu/lang/en_us.json
실행 시 마인크래프트 설정 내 조작에 들어가 보면 다음과 같이 키 바인딩이 추가된 것을 볼 수 있다.
또한 해당하는 키를 누를 때마다 콘솔 창에 다음 메시지가 뜨는 것을 볼 수 있다.
[23:29:12.030] [Client thread/DEBUG] [wi.na.my.in.NamuKeyInputHandler/INPUT]: Key binding = modnamu.key.set_spawn
[23:29:13.013] [Client thread/DEBUG] [wi.na.my.in.NamuKeyInputHandler/INPUT]: Key binding = modnamu.key.kill
13. 월드 생성 추가
13.1. 광물 생성
1.13/1.14 이후월드에 자신이 만든 광물이 생성되게 하고 싶다면
Biome
인스턴스의 addFeature()
메소드를 호출하면 된다. 이 메소드는 광물 생성 외에도 여러 구조물이나 식물 등을 생성하게 할 때 사용할 수 있다. 마인크래프트 기본 생성 설정은 DefaultBiomeFeatures
클래스에서 볼 수 있다.addFeature()
메소드의 파라미터는 다음과 같다.#!syntax java
void addFeature(GenerationStage.Decoration decorationStage, ConfiguredFeature<?> featureIn)
decorationStage : 해당 바이옴 특성의 종류를 나타낸다.
광물 생성의 경우 GenerationStage.Decoration.UNDERGROUND_ORES를 사용한다.
featureIn : 해당 바이옴 특성의 설정이다.
직접 ConfiguredFeature 클래스를 인스턴스화 할 수도 있지만 Biome.createDecoratedFeature() 메소드로 만드는 것이 편하다
Biome.createDecoratedFeature()
메소드를 사용하면 ConfiguredFeature
인스턴스를 편하게 만들 수 있다. 이 메소드의 파라미터는 다음과 같다.#!syntax java
static <F extends IFeatureConfig, D extends IPlacementConfig> ConfiguredFeature<?> createDecoratedFeature(Feature<F> featureIn, F config, Placement<D> placementIn, D placementConfig)
featureIn : 해당 특성이 생성할 것을 나타낸다.
일반적인 광물의 경우 Feature.ORE을 사용한다(예외적으로, 에메랄드는 Feature.EMERALD_ORE 사용).
config : 해당 특성이 생성할 오브젝트와 생성 위치 등을 설정한다.
일반적인 광물의 경우 OreFeatureConfig 인스턴스를 사용한다.
placementIn : 해당 특성이 생성할 오브젝트의 분포를 나타낸다.
Placement 클래스 내에 여러가지 분포가 정의되어 있으며, 바닐라 광물은 청금석과 에메랄드를 제외하면 Placement.COUNT_RANGE를 사용한다.
placementConfig : 오브젝트의 분포 설정이다. 일반적으로 광물은 CountRangeConfig 인스턴스를 사용한다.
OreFeatureConfig
클래스의 생성자에는 다음의 파라미터가 들어간다.#!syntax java
OreFeatureConfig(OreFeatureConfig.FillerBlockType target, BlockState state, int size)
target : 광물이 제거하고 대신 생성될 블록의 타입이다.
돌을 의미하는 NATURAL_STONE과 네더랙을 의미하는 NETHERRACK 두가지 타입이 있으며, 무슨 이유에서인지 아직 엔드 돌은 없다.
state : 생성할 광물 블록의 BlockState다. 해당 블록의 getDefaultState() 메소드를 사용하여 얻는다.
size : 한 번에 생성할 광물의 최대 개수이다.
CountRangeConfig
클래스의 생성자에는 다음의 파라미터가 들어간다.#!syntax java
CountRangeConfig(int count, int bottomOffset, int topOffset, int maximum)
count : 한 청크 내에서 광물 생성을 시도할 횟수이다.
bottomOffset : 광물이 생성될 최소 y 좌표와 비슷한 의미이다. 하지만 완전히 일치하지는 않는데, 그 이유는 광물 생성 알고리즘에서 설명한다.
topOffset : 광물이 생성될 최대 y 좌표의 감소량이다. 이 역시 정확한 의미는 광물 생성 알고리즘에서 설명한다.
maximum : 광물이 생성될 y 좌표의 범위이다. 실제로는 maximum - topOffset 만큼의 범위에서 생성된다.
월드에 자신이 만든 광물이 생성되게 하고 싶다면 마인크래프트 내의 모든 바이옴에
addFeature()
메소드를 사용하여 적절히 설정해주면 된다. 전체 바이옴 리스트는 ForgeRegistries.BIOMES
이다.#!syntax java
package wiki.namu.mymod.world;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.gen.GenerationStage;
import net.minecraft.world.gen.feature.Feature;
import net.minecraft.world.gen.feature.OreFeatureConfig;
import net.minecraft.world.gen.placement.CountRangeConfig;
import net.minecraft.world.gen.placement.Placement;
import net.minecraftforge.fml.Logging;
import net.minecraftforge.registries.ForgeRegistries;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import wiki.namu.mymod.NamuMain;
public class NamuOreGeneration
{
private static final Logger LOGGER = LogManager.getLogger();
public static void init()
{
ForgeRegistries.BIOMES.forEach(biome → {
biome.addFeature(
GenerationStage.Decoration.UNDERGROUND_ORES,
Biome.createDecoratedFeature(
Feature.ORE,
new OreFeatureConfig(OreFeatureConfig.FillerBlockType.NATURAL_STONE, NamuMain.RegistryEvents.NAMU_BLOCK.getDefaultState(), 15),
Placement.COUNT_RANGE,
new CountRangeConfig(20, 1, 0, 64)
)
);
LOGGER.debug(Logging.LOADING, "Additional ore generation of block "
+ NamuMain.RegistryEvents.NAMU_BLOCK.getRegistryName().toString() + " in biome "
+ biome.getRegistryName().toString() + " is registered");
});
}
}
OreFeatureConfig.FillerBlockType
를 NATURAL_STONE
으로 설정했기 때문에 해당 광물은 돌이 자연적으로 생성되는 바이옴에서만 생성된다. 즉, 따로 설정을 하지 않아도 엔드나 네더에서는 생성되지 않는 것이다. 만약 지옥에 광물을 생성하고 싶으면 OreFeatureConfig.FillerBlockType
파라미터를 NETHERRACK
로 바꾸면 된다. 무슨 이유에서인지는 몰라도 엔드 돌은 설정에 없기 때문에 엔드에 광물을 생성할 방법은 아직 없다.CountRange
의 알고리즘에 대한 설명을 간단히 하자면 다음은 해당 클래스 내의 광물 생성 위치 계산 코드다.#!syntax java
public Stream<BlockPos> func_212852_a_(Random p_212852_1_, CountRangeConfig p_212852_2_, BlockPos p_212852_3_) {
return IntStream.range(0, p_212852_2_.count).mapToObj((p_215061_3_) → {
int i = p_212852_1_.nextInt(16);
int j = p_212852_1_.nextInt(p_212852_2_.maximum - p_212852_2_.topOffset) + p_212852_2_.bottomOffset;
int k = p_212852_1_.nextInt(16);
return p_212852_3_.add(i, j, k);
});
}
즉, 광물의 y 좌표는 0부터 maximum - topOffset - 1 까지의 랜덤한 숫자에 bottomOffset을 더한 숫자로 결정된다. 예를 들어 maximum이 64이고, top Offset이 5, bottomOffset이 5라면 y 좌표는 5 ~ 63 사이의 랜덤한 숫자가 되는 것이다.참고로,
CountRangeConfig
를 사용하는 Placement
설정은 다음의 네가지가 있다.COUNT_RANGE : 일반적으로 광물 생성에 사용되며, 범위 사이에서 y 좌표에 따라 거의 균일하게 생성된다.
COUNT_BIASED_RANGE : 범위 사이에서 y 좌표가 낮아질 수록 생성 빈도가 높아진다.
COUNT_VERY_BIASED_RANGE : 범위 사이에서 y 좌표가 낮아질 수록 생성 빈도가 매우 높아진다.
RANDOM_COUNT_RANGE : COUNT_RANGE와 비슷하지만 한 청크당 생성 시도 횟수가 랜덤으로 결정된다.
따라서 운이 없으면 그 청크 내에 광물이 생성되지 않을 수도 있다.
COUNT_BIASED_RANGE
는 COUNT_RANGE
에서 랜덤으로 나온 숫자를 다시 랜덤 함수에 넣어 랜덤한 숫자를 뽑아낸다. 따라서 작은 숫자가 나올 빈도가 증가하는 것이다. COUNT_VERY_BIASED_RANGE
는 한 번 더 랜덤 함수에 넣기 때문에 빈도가 더 많이 증가하게 된다. bottomOffset = 1, topOffset = 0, max = 64를 사용했을 때, y 좌표에 따른 대략적인 생성 빈도는 다음과 같다. 아래는 생성 시도를 5000번 시도했을 때 나타나는 각 y 좌표의 횟수이다.하지만 월드의 밑바닥은 기반암으로 이루어져 있기 때문에 설정에 따라
COUNT_BIASED_RANGE
와 COUNT_VERY_BIASED_RANGE
의 광물 생성량이 더 적을 수도 있다.한가지 유의할 점은
COUNT_BIASED_RANGE
와 COUNT_VERY_BIASED_RANGE
를 사용할 때 bottomOffset을 0으로 두면 에러가 난다는 점이다. 따라서 이 때는 bottomOffset을 1 이상의 정수로 설정해야 한다.광물 생성 설정 코드를 짰다면 메인 클래스의
FMLCommonSetupEvent
의 리스너에서 이를 실행하도록 한다.#!syntax java
private void setup(final FMLCommonSetupEvent event)
{
LOGGER.info(Logging.LOADING, "Setup method registered");
NamuOreGeneration.init();
}
게임을 실행해서 월드를 생성하면 다음과 같이 광물이 생성된 것을 볼 수 있다. 다음 두 사진은 각각
COUNT_RANGE
와 COUNT_BIASED_RANGE
를 사용했을 때 같은 시드에서 같은 장소의 광물 분포이다. COUNT_BIASED_RANGE
쪽이 확연하게 아래쪽에 광물이 많이 모여있음을 알 수 있다.13.1.1. 사용자 정의 광물 배치 패턴 설정
Placement
클래스 내에 사전 정의된 패턴 외에 다른 광물 배치 패턴을 직접 정의해서 사용하는 방법은 비교적 간단하다. Biome.createDecoratedFeature()
메소드의 마지막 두 파라미터인 placementIn
과 placementConfig
를 상속을 이용해 정의한 후 인스턴스화하여 전달하면 된다. 경우에 따라서는 PlacementConfig
는 이미 있는 것을 사용할 수도 있다. 여기서는 둘 다 정의하는 예시를 들도록 하겠다.placementConfig
의 자료형인 D
는 IPlacementConfig
인터페이스를 구현한다. 이 인터페이스에는 다음의 메소드 하나가 정의되어 있다.#!syntax java
<T> Dynamic<T> serialize(DynamicOps<T> p_214719_1_)
이 메소드는 해당 컨피그 클래스의 필드를
Dynamic
이라는 여러 자료형의 변수를 하나로 모아 저장하는 클래스의 인스턴스에 저장한 후 이를 반환하는 역할을 한다. 이 메소드의 구현 방법은 다음과 같다.1.
Dynamic<>
의 생성자를 호출하여 인스턴스화 한다.2.
Dynamic<>
의 생성자는 다음과 같다.#!syntax java
Dynamic(final DynamicOps<T> ops, @Nullable final T value)
여기서 ops
에는 serialize()
메소드의 파라미터로 넘겨받은 것을 전달해주면 된다.3.
value
파라미터에는 해당 컨피그 클래스의 필드 정보를 저장하는 맵을 넘겨주면 된다. 이 작업은 ops.createMap(ImmutableMap.of())
으로 한다.4.
ImmutableMap.of()
메소드에는 필드의 이름과 값을 번갈아가며 전달해주면 된다. 이 때, 이름은 ops.createString()
메소드로 박싱해서 전달하고, 값은 ops.create<자료형>()
메소드로 박싱해서 전달한다. 예를 들어 int형 필드를 전달하고 싶다면 ops.createInt()
메소드를 사용한다. 기본 자료형에 해당하는 메소드는 모두 정의되어 있다. 설정에 따라 달라지는 필드만 전달하면 된다.예를 들어 어느 두 높이를 중심으로 양쪽으로 일정 범위 내에만 광물이 생성되게 하여 마치 광물 분포가 낙타 등 모양처럼 하는 분포의 컨피그 클래스를 만든다고 생각해보자. 이 예시에서는 클래스의 이름을
CamelPlacementConfig
로 지었다.#!syntax java
package wiki.namu.mymod.world.placement;
import com.google.common.collect.ImmutableMap;
import com.mojang.datafixers.Dynamic;
import com.mojang.datafixers.types.DynamicOps;
import net.minecraft.world.gen.placement.IPlacementConfig;
public class CamelPlacementConfig implements IPlacementConfig
{
public final int count; //한 청크 당 생성 시도 횟수
public final int lowerControlLine; //아래쪽 기준선
public final int upperControlLine; //위쪽 기준선
public final int lowerWidth; //아래쪽 기준선을 중심으로 양쪽으로 광물을 생성할 범위
public final int upperWidth; //위쪽 기준선을 중심으로 양쪽으로 광물을 생성할 범위
public CamelPlacementConfig(int count, int lowerControlLine, int upperControlLine, int lowerWidth, int upperWidth)
{
this.count = count;
this.lowerControlLine = lowerControlLine;
this.upperControlLine = upperControlLine;
this.lowerWidth = lowerWidth;
this.upperWidth = upperWidth;
}
@Override
public <T> Dynamic<T> serialize(DynamicOps<T> ops)
{
return new Dynamic<>(ops, ops.createMap(ImmutableMap.of(
ops.createString("count"), ops.createInt(count),
ops.createString("lower_control_line"), ops.createInt(lowerControlLine),
ops.createString("upper_control_line"), ops.createInt(upperControlLine),
ops.createString("lower_width"), ops.createInt(lowerWidth),
ops.createString("upper_width"), ops.createInt(upperWidth))));
}
}
필드를 전부 public final로 정의했는데, 그 이유는 나중에 Placement
상속체에서 광물 생성 위치를 구하는 코트에서 위의 필드를 직접 읽을 수 있게 하기 위함이다. serialize()
메소드 내의 ImmutableMap.of()
메소드의 파라미터로 필드의 이름과 필드값을 번갈아가며 전달해줬다. 이때, 필드 이름은 해당 필드의 이름을 전부 소문자로 바꾼 뒤 띄어쓰기는 언더바로 해주는 것이 좋다.컨피그 클래스를 구현했다면 이제
Placement
클래스를 상속하여 실제 광물 분포를 결정하는 클래스를 만들어야 한다. 실제로는 Placement
클래스의 상속체인 SimplePlacement<DC extends IPlacementConfig>
클래스를 상속하도록 한다. 여기서 DC는 해당 클래스에서 사용한 컨피그 클래스의 자료형을 넘겨주면 된다. 이 예시에서는 아까 만든 CamelPlacementConfig
로 하면 된다.주의할 점은
SimplePlacement<DC extends IPlacementConfig>
클래스의 기본 생성자는 Function<Dynamic<?>, ? extends DC>
형 인스턴스를 파라미터로 받는다. 즉, Dynamic
인스턴스에 저장된 필드 정보를 다시 언박싱해서 원래 컨피그 클래스의 인스턴스를 반환하는 함수형 인터페이스 Function
의 구현체를 받는다. 따라서 아까 만든 컨피그 클래스에 Dynamic
인스턴스를 받아 자기 자신의 인스턴스를 반환하는 정적 메소드를 만들 필요가 있다.#!syntax java
public static CamelPlacementConfig deserialize(Dynamic<?> dynamic)
{
int count = dynamic.get("count").asInt(0);
int lowerControlLine = dynamic.get("lower_control_line").asInt(0);
int upperControlLine = dynamic.get("upper_control_line").asInt(0);
int lowerWidth = dynamic.get("lower_width").asInt(0);
int upperWidth = dynamic.get("upper_width").asInt(0);
return new CamelPlacementConfig(count, lowerControlLine, upperControlLine, lowerWidth, upperWidth);
}
Dynamic
인스턴스에 저장된 필드를 다시 가져오는 방법은 get()
메소드에 serialize()
메소드에서 ops.createString()
으로 넘겨주었던 필드 이름을 넘겨주고, 이를 다시 as<자료형>()
메소드로 원래 자료형으로 변환하는 과정을 거치면 된다. 이 때, 파라미터로 저장된 값이 없을 경우의 기본값을 넘겨주면 되는데, 여기서는 모두 0으로 넣어줬다. 그다음 생성자를 사용해 자기 자신의 인스턴스를 리턴하면 된다.이제
SimplePlacement<DC extends IPlacementConfig>
를 상속하여 새로운 클래스를 만들면 된다. 이 때 구현해야 할 메소드는 하나다.#!syntax java
Stream<BlockPos> func_212852_a_(Random p_212852_1_, DC p_212852_2_, BlockPos p_212852_3_)
위 메소드는 광물이 생성될 위치를 BlockPos
의 스트림의 형태로 반환한다. 여기서 p_212852_3_는 청크 내에서 각 좌표가 가장 작은, 그러니까 가장 구석에 있는 블록의 위치다.#!syntax java
package wiki.namu.mymod.world.placement;
import com.mojang.datafixers.Dynamic;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.gen.placement.SimplePlacement;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class CamelPlacement extends SimplePlacement<CamelPlacementConfig>
{
public CamelPlacement(Function<Dynamic<?>, ? extends CamelPlacementConfig> configFactoryIn)
{
super(configFactoryIn);
}
@Override
public Stream<BlockPos> func_212852_a_(Random random, CamelPlacementConfig config, BlockPos pos)
{
return IntStream.range(0, config.count).mapToObj((num) → {
int i = random.nextInt(16);
int j;
int location = random.nextInt(2);
switch(location)
{
case 0:
j = config.lowerControlLine + random.nextInt(config.lowerWidth * 2 + 1) - config.lowerWidth;
break;
case 1:
j = config.upperControlLine + random.nextInt(config.upperWidth * 2 + 1) - config.upperWidth;
break;
default:
j = 0;
}
int k = random.nextInt(16);
return pos.add(i, j, k);
});
}
}
컨피그 파일의 count
필드의 수만큼 광물을 생성해야 하므로 IntStream.range()
메소드로 먼저 count
회 반복하는 정수 스트림을 만들었다. 그리고 mapToObj()
메소드에 각 정수를 받아 BlockPos
인스턴스를 리턴하는 람다식을 전달하여 BlockPos
스트림으로 변환했다. 한 청크 내에서 광물이 생성될 x, z 좌표는 0 ~ 15까지의 수 중 무작위로 뽑았다. y 좌표는 동일한 확률로 위쪽 또는 아래쪽에서 기준선을 기준으로 양 위아래로 설정한 너비만큼의 범위 내에서 생성되도록 정했다. 그리고 청크 내 가장 작은 좌표인 pos
에 add()
메소드로 위에서 만들어낸 x, y, z 좌표를 더한 뒤 리턴해줬다.두 클래스를 만들었으면 이제
Placement
의 상속체를 인스턴스화하여 정적 변수로 저장해두면 된다. 이 때, 생성자에 해당하는 컨피그 클래스의 deserialize()
메소드를 람다식의 형태로 넘겨준다.#!syntax java
public static final Placement<CamelPlacementConfig> CAMEL_PLACEMENT = new CamelPlacement(CamelPlacementConfig::deserialize);
그런 다음 createDecoratedFeature()
메소드의 마지막 두 파라미터를 자신이 만든 클래스의 인스턴스로 대체하면 된다.#!syntax java
public static void init()
{
ForgeRegistries.BIOMES.forEach(biome → {
biome.addFeature(
GenerationStage.Decoration.UNDERGROUND_ORES,
Biome.createDecoratedFeature(
Feature.ORE,
new OreFeatureConfig(OreFeatureConfig.FillerBlockType.NATURAL_STONE, NamuMain.RegistryEvents.NAMU_BLOCK.getDefaultState(), 15),
CAMEL_PLACEMENT,
new CamelPlacementConfig(20, 20, 50, 5, 6)
)
);
LOGGER.debug(Logging.LOADING, "Additional ore generation of block "
+ NamuMain.RegistryEvents.NAMU_BLOCK.getRegistryName().toString() + " in biome "
+ biome.getRegistryName().toString() + " is registered");
});
}
위 예시에서는 한 청크 내에서 20번 생성을 시도하며, y = 20을 기준으로 위아래로 5칸, y = 50을 기준으로 위아래로 6칸 범위 내에 광물이 생성되도록 했다. 마인크래프트를 실행하여 월드를 생성해보면 다음과 같은 광물 분포를 볼 수 있다.푸른 선으로 기준선을, 붉은 선으로 범위를 표시했다. 범위 밖에는 광물이 거의 없는 것을 볼 수 있다. 범위 밖으로 나온 광물은 광물이 생성되기 시작한 지점은 범위 내였지만 광맥이 범위 밖으로 침투한 것이다.
마인크래프트 기본 설정의 광물 배치 구현 코드는
Placement
클래스 내의 변수와 생성자들을 클릭하여 확인해볼 수 있으니 참고하길 바란다.13.2. 구조물 생성
구조물은 크게 잔디나 나무 등 자연 생성 구조물과 던전, 폐광과 같은 인공 구조물로 나뉜다. 자연 구조물과 작은 인공 구조물은 보통 청크를 장식할 때 생성되며, 여러 청크에 걸치는 큰 인공 구조물은 구조물 생성 시스템을 이용해 청크가 로딩될때마다 각 구역을 생성하는 방식으로 생성된다.구조물을 생성할 때 주의해야 할 점은 월드젠을 위해 로딩된 청크의 경계를 넘지 말아야 한다는 점이다. 그렇지 않을 경우 월드 구조물 생성이 인근 청크들을 로딩하여 추가적인 생성을 유발하는 연쇄 반응이 일어날 수 있으며, 최악의 경우 무한 루프에 빠져 크래시로 이어질 수 있다. 참고로 마인크래프트는 특정 청크 (X, Z)를 생성할 때 청크 좌표[16] (X+1, Z), (X, Z+1), (X+1, Z+1)에 해당하는 청크들도 같이 로딩하므로 이 안에서만 생성하면 된다. 청크 경계를 어쩔 수 없이 넘을 수 밖에 없는 큰 구조물의 경우 구조물 생성 시스템을 사용하거나 구역별로 나누어 생성하기 위한 별도의 코드를 작성해야 한다.
마인크래프트 1.11부터는 로딩된 청크를 넘어 구조물을 생성할 시 포지에서 경고 로그를 띄운다.
<모드 이름> loaded a new chunk <로딩된 청크 좌표> in dimension <차원 ID> (<차원 이름>) while populating chunk <생성중인 청크 좌표>, causing cascading worldgen lag. Please report this to the mod's issue tracker. This log can be disabled in the Forge config.
13.3. 바이옴 추가
13.4. 차원 추가
14. GUI
만약 화로, 작업대와 같은 인벤토리 GUI는 위의 블록 엔티티 항목의 GUI 참조.15. 각종 팁
15.1. 1.8 이후용 팁
- 1.7.10 및 이전 버전에서 사용되던 코드는, 1.8 이후에서는 대부분 호환되지 않는다.
- cpw가 포지 팀에서 탈퇴하여 cpw.mods.fml.common.*; 따위의 임포트는 오류를 내고, net.minecraftforge.fml.common.*; 로 임포트해야 한다.
- 아이템과 블록의 텍스쳐 및 모델 지정 방식이 변경되어 텍스쳐 제작으로 끝나지 않고, json 파일로 모델을 만들어야 한다. 그러나 json을 사용하지 않고 코드상에서 아이템과 블록의 모델을 추가하는것은 여전히 가능하다.[17]
- x, y, z 3개의 int 값으로 표현되던 좌표가 대부분 BlockPos 로 교체되었다.
- 블록의 방향, 면 등을 나타내기 위한 포지 클래스인 ForgeDirection은 바닐라에 새로 생긴 EnumFacing으로 대체되었다. 알 수 없는 방향을 나타내던 값인 ForgeDirection.UNKNOWN 은 null을 대신 사용해야 하므로 null 체크에 유의할 것.
- 블록의 상태를 나타내기 위한 정수값인 메타데이터가 IBlockState로 대체되었다. 블록 종류 또한 World.getBlock(int x, int y, int z) 대신 IBlockState.getBlock() 함수를 이용해서 구해야 한다.
- 1.12부터는 제작대 조합법, 포션, 바이옴, 사운드도 아이템과 블록처럼 레지스트리 이름을 설정하고나서 등록해야한다.
15.2. 1.13 이후용 팁
- 1.7.10 → 1.8과 마찬가지로, 1.12.2에서 사용되던 코드는 1.13 이후에서는 대부분 호환되지 않는다.
- 이전 버전들에 비해 JSON 파일을 만들어야 할 필요가 상당히 많아졌으며 특히 대규모 모드의 경우 수천개 이상의 JSON 파일이 필요할 수 있다. 블록과 아이템의 모델은 물론이고, 제작대 조합법 및 화로 레시피, 블록과 몹의 드롭 아이템, 아이템 태그(광석 사전 대체재) 등을 일일이 JSON 파일로 작성해야 하기 때문.[18] 물론 수많은 JSON 파일을 만드는 대신 코드상에서 자동 생성하는 방법도 여전히 가능하지만 코딩 난이도가 다소 어려우며, 코드상에서 레시피를 추가하면 데이터팩을 통한 아이템 제작법 변경이 어려워지므로 수많은 아이템의 레시피를 일정한 규칙으로 반복 생성해야 하는 경우가 아니라면 권장되지 않는다.
- 1.13부터는 Flattening 이라는 아이템/블록 데이터 시스템의 변경으로 인해 내부적으로 사용되던 숫자 ID와 개수 제한(아이템의 경우 32767개, 블록의 경우 4096개)이 제거되었으며, 아이템과 블록의 메타데이터도 삭제되었다. 염료나 양털같이 메타데이터로 구분되던 아이템은 별개의 아이템으로 나누어서 등록해야 한다.
- 광석사전은 바닐라에 유사한 기능인 아이템/블록 태그(Tag) 시스템이 추가됨에 따라 삭제되었다. 기존의 광석 사전과 달리 아이템 이름 형식 또한 다르므로 기존의 광석 사전에 "oreNamu"라고 등록되어 있던 아이템이 있다면, "forge:ores/namu" 태그를 생성한 후 등록시켜야 한다. 패브릭의 경우 이름공간만 패브릭으로 바꾸어 "fabric:ores/namu"로 등록하면 된다. 참고로 광석은 ores/, 조각은 nuggets/, 주괴는 ingots/, 보석은 gems/, 가루는 dusts/ 를 앞에 붙여야 한다. 타 모드와 공유하지 않고 자신의 모드에서 내부적으로 사용할 태그라면 "namumod:ores/namu"와 같이 자신의 모드 이름을 forge 대신 붙이면 된다. 또한 모드의 데이터팩에 태그의 json 파일을 생성해야 한다. {{{#!syntax java
public static Tag<Item> namuOre = new ItemTags.Wrapper(new ResourceLocation ("forge:ores/namu"));
}}} 태그의 JSON 파일의 코드이다. 여러개의 아이템을 한 태그에 지정하는 것도 가능하다. 파일 경로와 이름은 resources[19]/data/forge/tags/blocks/ores/namu.json (블록 태그), resources/data/forge/tags/items/ores/namu.json (아이템 태그)로 지정하면 된다. 블록과 아이템 태그는 별개이므로 둘 다 지정해야 인벤토리의 블록이 인식되지 않는 문제를 방지할 수 있다. {{{#!syntax json
"replace": false,
"values": [
}"values": [
"namumod:namu_ore"
]}}}
* 일부 클래스의 이름이 변경되고 클래스의 명명 방식도 반대로 바뀌었다. 예를 들어 EnumFacing 클래스는 Direction으로 바뀌었으며, EntityPlayer 클래스는 PlayerEntity로 바뀌었다.
* 일부 클래스의 이름이 변경되고 클래스의 명명 방식도 반대로 바뀌었다. 예를 들어 EnumFacing 클래스는 Direction으로 바뀌었으며, EntityPlayer 클래스는 PlayerEntity로 바뀌었다.
15.3. Access Transformer
영어 원문 설명서: https://mcforge.readthedocs.io/en/1.16.x/advanced/accesstransformers/기존 마인크래프트 코드에 존재하는 필드나 메소드의 접근 제한자를 변경할 수 있는 방법. 주로 private 나 protected 로 지정된 필드를 public 으로 바꾸어 외부에서도 접근할 수 있게 만드는 데 사용된다.
프로젝트를 처음 빌드할 때, build.gradle 파일을 수정하여 access transformer 에 대한 설정을 해줘야 한다. 대부분의 프로젝트는 access transformer에 대한 설정을 미리 주석 처리해 놓았으므로 이를 사용하고 싶으면 주석을 제거하기만 하면 된다.
minecraft {
// accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg') 대부분 이런 식으로 주석 처리되어 있다.
}
잠재적인 문제가 발생할 수 있으므로 주석 처리된 부분에 있는 파일명을 그대로 사용하는 것을 권장한다. 그런 다음 위에 경로와 동일한 위치에 파일을 생성하면 된다.이제 접근 제한자를 변경하고자 하는 필드의 난독화 이름을 알아내야 한다. 프로젝트를 빌드할 때 사용한 명령창에 gradlew createMcpToSrg 명령어를 입력하면 /build/createMcpToSrg/output.tsrg 에 모든 필드와 메소드의 난독화 이름을 기록한 파일이 하나 생성된다. 파일의 내용이 매우 방대하니 검색 기능을 적극 활용할 수 있는 편집기를 사용하는 것을 추천한다.
원하는 필드 또는 메소드의 난독화 이름을 알아내었다면, accesstransformer.cfg 파일에 기록하면 된다. 형식은 다음과 같다.
필드:
<변경하려는 접근제한자> <패키지 포함 클래스 이름> <난독화 이름>
메서드:
<변경하려는 접근제한자> <패키지 포함 클래스 이름> <난독화 이름>(<인자 타입>)<리턴 타입>
필드의 경우엔 단순히 난독화 이름만 적어넣으면 된다. 만약 Entity 클래스 내의 field_12345_a 라는 난독화 이름을 가진 private 필드를 public 으로 바꾸고 싶다면 다음과 같이 적으면 된다.
public net.minecraft.entity.Entity field_12345_a
메소드의 경우는 조금 복잡한데, 메소드의 난독화 이름과 그 인자값, 리턴 타입을 모두 적어줘야 한다. 다음은 실제 마인크래프트 ItemStack 클래스 내부에 있는 메소드이다.
#!syntax java
private boolean isItemStackEqual(ItemStack other) {
...
}
이 메소드는 ItemStack 객체를 인자로 받으며, boolean 값을 리턴한다. 이를 AccessTransformer가 인식할 수 있는 이름으로 바꾸어 주어야 하는데, 원시 타입들의 이름들은 다음과 같다.
B - byte,
C - char,
D - double,
F - float,
I - integer,
J - long,
S - short,
Z - boolean
클래스 이름은 "L(패키지 경로 포함 클래스 이름);" 으로 표현한다. 만약 ItemStack 클래스를 표현하려면 "Lnet/minecraft/item/ItemStack;" 이 하나의 클래스 이름이 된다. 위의 예시로 든 메소드를 AccessTransformer가 인식할 수 있는 이름으로 바꾸면 다음과 같다.
public net.minecraft.item.ItemStack func_77959_d(Lnet/minecraft/item/ItemStack;)Z
이런 식으로 AccessTransform에 원하는 필드/메소드를 기록하고 프로젝트를 재빌드하면 접근 제한자가 바뀌어 있는 것을 볼 수 있다. 이러면 모드 개발자의 영역에서도 기존 마인크래프트 클래스 내의 변수들을 자유롭게 접근/수정할 수 있다.
물론 이 기능도 만능은 아니어서, 상속된 클래스에서 오버라이드한 private 메소드의 접근 제한자를 public 으로 변경하려고 하면 자바의 문법 규칙을 위반하게 되어 오류가 일어난다. 또한 여러 모드 간 호환성 문제에서도 자유롭지 못하기 때문에 최대한 다른 방법을 시도하다 최후의 수단으로 사용하는 것을 권장한다.
15.4. 기타 팁
- 리플렉션(Reflection)을 사용하면 private 등 접근 제한자로 인해 일반적으로는 접근할 수 없는 필드나 메소드에 접근할 수 있다. 그러나 마인크래프트는 난독화되지 않은 개발 환경과 난독화된 버전으로 나뉘므로, 리플렉션을 사용한 코드가 양쪽에서 오류없이 동작하도록 하려면 포지에서 제공하는 ObfuscationReflectionHelper 라는 클래스를 활용하는 것이 유용하다. 또한 private로 설정된 필드와 메소드중에는 함부로 접근하면 문제가 생길 수 있기 때문에 제한한 것들도 있으므로 유의해서 사용해야 한다. 또한 리플렉션을 이용하면 해당 모드의 API나 IMC[20]를 사용하지 않고도 타 모드의 기능을 건들일 수 있다. 그러나 해당 모드가 설치되지 않으면 클래스를 찾지 못하여 오류가 발생하므로 해당 모드가 설치된 경우에만 동작하게 해야 한다. Reflection 사용시 성능이 떨어진다는 주장도 있으나 Reflection은 처음 JVM에서 로딩시에만 오버헤드가 발생할 뿐, 추가로 호출 시 별 차이 없다. 성능 테스트 환경을 불합리하게 설정 또는 오버헤드가 발생되게끔 사용해서 성능이 떨어지는것이다.
게임 버전 1.17 이후부터는 자바 16 이상을 사용함에 따라 리플렉션을 사용하여도 접근 제한자로 제한된 필드와 메소드에 접근하기 어렵게 되었다. Access Transformer 등 다른 방법을 사용하는 것이 권장된다. - 애드온 등 타 모드를 지원하는 모드를 개발할 경우 소스코드 폴더 내에 타 모드의 API를 넣는 대신 Java Build Path를 이용하여 빌드 경로에 API를 대신 포함시키는 것이 권장된다. 모드 JAR 파일 내에 타 모드의 API가 포함되어 있을 때 해당 모드의 API가 업데이트되면 충돌이 발생할 수 있다.
- 바닐라 또는 타 모드의 소스코드를 수정할 필요가 있다면 소스코드를 직접 뜯어고치는 대신 ASM을 사용하는 것이 권장된다. ASM은 자바 바이트코드를 조작하여 소스코드를 수정한 효과를 내는 기능이다. 그러나 자바 바이트코드에 대한 이해가 필요하며, 타 모드와의 호환성 문제가 발생하거나 디버깅이 어려워질 수 있으므로 반드시 필요한 경우에만 사용하는 것이 권장된다. Mixin API를 사용하면 보다 손쉽게 ASM을 사용 가능하며, 특히 패브릭 개발 환경에서는 기본적으로 Mixin을 지원한다.
16. 자바가 뭐예요?
아무래도 위 설명은 프로그래밍을 하나도 모르는 사람이 보면 상당히 어렵게 느껴질 것이다.하지만 다행히도 프로그래밍과의 조우가 없는 일반인들도 손쉽게 간단한 모드를 만들 수 있는 툴이 존재한다. 보통 어려운 프로그래밍을 대신 해주는 개념으로, 예를 들어 블록을 만들고 싶다 하면, 원하는 블록에 대한 정보를 입력하면 '알아서' 프로그래밍해서 모드로 변환해준다. 툴에 따라서 범용성 높은 기능을 통해 훨씬 더 복잡한 모드도 충분히 만들 수 있고 모드 제작에 입문하는 데 큰 도움을 주니, 관심 있다면 한 번 해보자.
다만, 수준 높고 세심한 툴일 수록 계속해서 업데이트되고, 제작자가 외국인인 경우가 대다수라 한글 지원은 힘들다.
16.1. 모드메이커
일반적인 블록, 액체 등의 단순한 모드를 쉽게 만들 수 있고, 사용자 환경도 단순해 처음 접해보는데 문제가 없다. 경우에 따라 한글로 패치된 버젼도 있어 잘 찾아보자. 다만, 복잡한 모드를 만들기는 쉽지 않다.16.2. MCreator
자세한 내용은 MCreator 문서 참고하십시오.17. 관련 문서
[1] Risugami 모드로더, MCP를 이용하거나 직접 난독화된 코드를 해석하는 무식한 방법 등이 있다.[2] 1.17부터 존재한다.[3] 한국어의 경우 번역기 티가 나는 편이다.[4] 이상하게 설치후 하단에 올바른 위치가 아니인 다른 위치에 몇몇 jar 파일이 연결된 현상이 보인다. 이때는 해당 오류를 우클릭해 빠른 고치기를 통해서 수동으로 수정해줘야 작동한다.[5] 게임 안에서 아이템을 손에 들거나 인벤토리에서 아이템에 커서를 대면 나오는 이름[6] 해당 메서드가 앞으로 변경되거나 없어지니 사용을 자제하라는 일종의 경고문[7] 이클립스를 포함한 대부분의 IDE는 임포트 단축키가 내장되어 있다.[8] 마인크래프트 1.11 부터는 모드 아이디에 대문자를 쓸 수 없다.[9] 모드 목록에 자신의 모드가 뜨지 않는다면 포지에서 모드 등록 중 오류가 생겼을 가능성이 높다. 콘솔에서 오류 로그를 찾아 문제를 추적해보자.[10] 이 현상은 싱글 플레이 월드를 플레이할 때 일어나는데, 싱글 플레이에서 LAN 서버를 여는 기능이 있기 때문이다.[11] 오버라이드는 프로그래밍 용어로, 부모 클래스에 이미 존재하는 메소드를 다시 정의해서 덮어씌우는 것을 말한다.[12] 이것들만 잘 읽어봐도 자신이 원하는 블록은 대부분 만들어낼 수 있다.[13] 만약 블록을 정의하지 않은 유체 블록 클래스에 getBlock() 메소드를 호출하면 NullPointerException 크래시가 뜬다.[14] 위는 프록시 클래스를 사용한 예제이다. 프록시(Proxy)라는 의미 자체가 대리인이듯, 아이템이나 블록같은 것을 등록해야할 때 메인 코드에서 이벤트 처리를 건네받아 대신 처리해주는 역할을 함으로써 메인 코드의 번잡함을 막을 수 있다.[15] 예를 들면 아이템 또는 블록을 리턴하는 함수로 등록을 시도할 때 아이템이 유동적으로 만들어지는 구조일 경우에는 해당 아이템 또는 블록을 찾을 수 없다. 대표적으로 묘목이 이런 경우.[16] 청크 좌표는 블록 좌표의 16분의 1이다. 따라서 오류가 발생한 청크로 이동하려면 로그에 표시된 X,Z 좌표 각각에 16을 곱해주어야 한다.[17] 주로 json 모델만으로는 한계가 있는 블록/아이템 모델을 만들기 위해서 또는 json 파일로 하면 너무 파일 숫자가 많아지는 경우에 코드상에서 모델을 자동 생성하기 위해 사용된다.[18] 예컨데, 일반적인 모델과 드롭 아이템을 가진 블록 하나를 추가하려면 블록 상태, 블록 모델, 아이템 모델, 드롭 아이템을 지정하는 4개의 JSON 파일을 새로 생성해야 하며, 언어 JSON 파일에 블록 이름을 추가해야 한다.[19] 텍스처나 모델 등을 보관하는 assets 폴더의 상위 폴더이다.[20] Inter-Mod Communication, 포지에 있는 모드간 소통 기능이다.