
플러그인은 결국 여러 API의 조합이다. 하나씩 뜯어보자.
이런 분들께 추천합니다
- EP5에서 Hello World 플러그인을 만들고, "이제 뭘 더 만들 수 있지?" 하는 분
- Paper API가 뭔지는 들어봤는데 어디서부터 공부해야 할지 모르겠는 분
- 플러그인이 어떻게 동작하는지 알고싶으신 분
요약
| 문제 | Hello World는 만들었는데, 이걸로는 아무것도 못 한다 |
| 시도 | Paper API의 핵심 개념을 익히고, 간단한 미니게임 플러그인을 만들어보자 |
| 해결 | Paper API 핵심 + 미니 플러그인으로 바로 체감! |
0. 먼저, 완성본을 보여드립니다
이번 시리즈에서 최종적으로 만어볼 게임입니다.
Fall Guys의 Hex-A-Gone처럼, 밟으면 바닥이 사라지는 마인크래프트 미니게임입니다.
미니게임을 진행하기 위한 최소한의 필수 기능(MVP)만 만든 상태입니다.
앞으로 몇 편에 걸쳐 이걸 만들어보겠습니다.
(현재는 대략적인 큰 그림만 구현됨)

MVP란 Minimum Viable Product)의 약자로 "일단 돌아가는 최소 버전" 입니다.
처음부터 완벽하게 만들지 않고, 핵심 기능만 실제로 만들어서 개선해나가는 방식입니다.
1. How Plugins Work — 큰 그림 잡기
EP5에서 Hello World 플러그인을 만들었습니다.
플레이어가 접속하면 "Hello, 닉네임!"을 띄우는 간단한 플러그인이었죠.
이제 "Hello World 수준"을 넘어서려면, Paper API가 어떤 기능을 제공하는지 알아야 합니다.
Paper 공식 문서의 How do plugins work? 섹션을 기반으로 정리하겠습니다.
문서 보는 법
일단 Paper API 문서는 두 곳입니다.
| 사이트 | 역할 | 주소 |
| Paper Docs | 가이드, 튜토리얼, 개념 설명 | docs.papermc.io |
| Javadoc | API 레퍼런스 (클래스, 메서드 목록) | jd.papermc.io |
두 사이트를 비교해자면,
Docs는 "이 기능을 어떻게 쓰는지" 알려주고, Javadoc은 "이 클래스에 어떤 메서드가 있는지" 알려줍니다.
예를 들어서, Docs는 안내서이고 Javadoc은 사전이라고 보시면 됩니다.
안내서로 개념을 잡고, 디테일한 부분이 필요하면 사전을 찾아보는겁니다.
아무리 복잡한 플러그인을 만들더라도, 결국 Paper에서 제공하는 API의 조합입니다.
공식 문서에 나와있는 설명을 보며 6가지 개념을 훑어보고 가겠습니다.
- Plugin Lifecycle
- Event Listener
- Command
- Config
- Scheduling Task
- Component
하나씩 살펴보겠습니다.
1-1. Plugin Lifecycle — "플러그인이 켜지고 꺼진다"
서버가 시작되면 plugins/ 폴더의 .jar 파일을 찾아서 로드합니다.
플러그인의 생명주기는 세 단계입니다:
서버 시작 → .jar 로드 → onLoad() -> onEnable() 실행
↓
서버 운영 중
↓
플러그인 비활성화 → onDisable() 실행
| 단계 | 메서드 | 설명 |
| Initialization | onLoad() | 플러그인이 메모리에 올라갈 때. 대부분의 API를 아직 못 씀 |
| Enabling | onEnable() | 이벤트·명령어 등록. 서버가 틱을 시작하기 전에 호출 |
| Disabling | onDisable() | 자원 정리, 데이터 저장, DB 연결 끊기 |
EP5에서 onEnable()에 로그 한 줄만 넣었던 걸 기억하시나요?
실제 미니게임에서는 여기서 모든 준비를 합니다:
@Override
public void onEnable() {
saveDefaultConfig(); // config.yml 로드
registerEvents(new BlockListener()); // 이벤트 등록
getCommand("dd").setExecutor(...); // 명령어 등록
getLogger().info("미니게임 플러그인 활성화!");
}
@Override
public void onDisable() {
arenaBuilder.restoreArena();
getLogger().info("미니게임 플러그인 비활성화!");
}
1-2. Event Listener — "무슨 일이 생기면 감지한다"
마인크래프트 서버에서는 매 순간 수많은 이벤트가 발생합니다.
플레이어가 움직이면, 블록을 부수면, 몬스터가 스폰되면 — 전부 이벤트입니다.
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
event.getPlayer().sendMessage(Component.text("환영합니다!"));
}
EP5의 Hello World에서 PlayerJoinEvent를 사용했습니다. 이것이 이벤트 리스닝이었습니다.
| 이벤트 | 발생 시점 |
| PlayerJoinEvent | 플에이어가 접속할 때 |
| PlayerMoveEvent | 플레이어가 움직일 때 |
| BlockBreakEvent | 블록을 부술 때 |
| PlayerDropItemEvent | 아이템을 버릴 때 |
| 그 외 Event | 수 많은 종류의 마인크래프트 Event가 발생할 때 |
사용법을 미리 말씀드리자면 간단합니다:
- 클래스에
implements Listener선언 - 메서드 위에
@EventHandler어노테이션 추가 onEnable()에서registerEvents()로 등록
public class MyListener implements Listener {
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
// 플레이어가 움직일 때마다 실행
}
}
public class ExamplePlugin extends JavaPlugin {
@Override
public void onEnable() {
// onEnable()에서 등록
getServer().getPluginManager().registerEvents(new MyListener(), this);
}
}
이번 미니게임에서: 블록 밟기 감지, 낙사 판정, 아이템 드롭 차단 등에 사용합니다.
성능 주의:
PlayerMoveEvent는 "핫 이벤트"입니다.
플레이어가 마우스를 돌리기만 해도 발생하는, 서버에서 가장 자주 호출되는 이벤트 중 하나입니다.
여기에 무거운 로직을 넣으면 서버가 연산을 많이 해야하기 때문에 느려집니다.
(사용 시 조건에 안 맞으면 즉시return하는 패턴(연산 가볍게 만들기)이 중요합니다.)
— Event Listeners — PaperMC Docs
1-3. Command — "플레이어가 명령어를 친다"
채팅창에 /명령어를 치면 뭔가를 실행하는 기능입니다.
등록 과정은 두 단계입니다:
① plugin.yml에 명령어 선언:
commands:
mycommand:
description: 내 커맨드 설명
usage: /mycommand
② CommandExecutor 구현:
public class MyCommand implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("플레이어만 사용할 수 있습니다.");
return true;
}
player.sendMessage(Component.text("명령어가 실행되었습니다!"));
return true;
}
}
// onEnable()에서 등록
getCommand("mycommand").setExecutor(new MyCommand());
이번 미니게임에서:
/mycommand join(참가),/mycommand leave(나가기),/mycommand start(시작) 등
미니게임을 위한 기능에 사용합니다.
1-4. Config — "설정을 파일로 관리한다"
미니게임 플러그인의 게임 시작 최소 인원이 2명인데, 나중에 4명으로 바꾸고 싶다면?
이런 값은ㅇ게임의 로직이 아니라 설정입니다. 코드에 직접 쓰는 게 아니라 config.yml에 분리하면 코드 수정 없이 설정만 바꿔서 서버를 재시작하면 됩니다.
즉, 개발자가 아닌 서버 관리자(운영자)가 코드를 몰라도 설정을 조정할 수 있습니다.
src/main/resources/config.yml:
game:
min-players: 2
코드에서 읽기:
@Override
public void onEnable() {
saveDefaultConfig(); // config.yml이 없으면 기본 파일 생성
int minPlayers = getConfig().getInt("game.min-players", 2); // 기본값 2
}
| 코드 | 하는 일 |
| saveDefaultConfig() | 최초 실행 시 기본 config.yml 복사. 이미 있으면 무시 |
| getConfig().getInt(...) | config.yml에서 값을 읽음. 없으면 두 번째 인자(기본값) 사용 |
미니게임에서: 블록이 사라기지 전의 블록 종류, 최소 인원 등을 설정합니다.
1-5. Scheduling Task — "N초 뒤에 실행한다"
마인크래프트는 게임 루프를 반복 실행하면서 돌아갑니다.
이 루프가 한 번 실행되는 것을 틱(Tick) 이라 하고, 1초에 20틱입니다.
| 틱 | 시간 |
| 20 | 1초 |
| 60 | 3초 |
| 100 | 5초 |
스케줄러 가져오기:
@Override
public void onEnable() {
BukkitScheduler scheduler = this.getServer().getScheduler();
}
N틱 뒤에 한 번 실행:
scheduler.runTaskLater(plugin, () -> {
player.sendMessage(Component.text("1초가 지났습니다!"));
}, 20); // 20틱 = 1초
N틱마다 반복 실행:
scheduler.runTaskTimer(plugin, () -> {
// 매초 실행되는 코드
}, 0, 20); // 0틱 후 시작, 20틱(1초)마다 반복
반복 중에 취소:
scheduler.runTaskTimer(plugin, task -> {
// 매초 실행되는 코드
if (조건) {
task.cancel(); // 조건이 맞으면 반복 중지
}
}, 0, 20);
| 메서드 | 하는 일 |
| runTaskLater(plugin, 코드, 딜레이) | 딜레이 후 한 번 실행 |
| runTaskTimer(plugin, 코드, 초기딜레이, 간격) | 간격마다 반복 실행 |
| task.cancel() | 반복 실행 중지 |
미니게임에서: 블록 사라짐 딜레이(밟고 1초 후 사라짐), 카운트다운 타이머(3, 2, 1, START!)에 사용합니다.
1-6. Component — "예쁜 텍스트와 연출"
Paper 1.21에서는 Adventure API를 사용해서 텍스트를 꾸밉니다.
MiniMessage — 컬러 텍스트
import net.kyori.adventure.text.minimessage.MiniMessage;
var mm = MiniMessage.miniMessage();
player.sendMessage(mm.deserialize("<gold><bold>승리!</bold> 축하합니다!"));
마크다운처럼 태그로 스타일을 지정합니다:
Title — 화면 중앙에 큰 글자
import net.kyori.adventure.title.Title;
player.showTitle(Title.title(
Component.text("3").color(NamedTextColor.YELLOW), // 메인 텍스트
Component.empty() // 서브 텍스트
));
Sound — 사운드 재생
player.playSound(Sound.sound(
Key.key("entity.player.levelup"),
Sound.Source.MASTER,
1.0f, // 볼륨
1.0f // 피치
));
미니게임에서: 카운트다운 타이틀("3", "2", "1", "START!"), 탈락 메시지, 우승 연출에 사용합니다.
1-7. 정리
| 개념 | 한 줄 요약 | |
| ① | Lifecycle | 플러그인이 켜지고 꺼진다 |
| ② | Event Listener | 무슨 일이 생기면 감지한다 |
| ③ | Command | 명령어를 치면 실행한다 |
| ④ | Config | 설정을 파일로 관리한다 |
| ⑤ | Scheduler | N초 뒤에 실행한다 |
| ⑥ | Component | 예쁜 텍스트와 연출 |
튜토리얼 페이지에서 보이는 걸 위주로 정리했습니다.
그 외에도 더 많은 API나 기능이 있으니 공식문서를 한번 보셔서 확인하는게 좋습니다.
2. API 조합 -> 미니 플러그인 만들기
세션 1에서 배운 6가지 개념을 전부 사용하는 플러그인을 만들어봅시다.
2-1. 뭘 만들까?
세션 1에서 본 개념을 전부 사용하는 플러그인을 만들어봅시다.
"밟으면 블록이 사라지는 플러그인" — 세션 0에서 보여드린 미니게임의 핵심 기능입니다.
단순히 블록만 사라지는 게 아니라, 명령어로 켜고 끄고, 설정 파일로 블록 종류를 바꾸고, 사라진 블록 수까지 화면에 보여주는 플러그인입니다.
| 기능 | 설명 | 사용하는 개념 |
| 플러그인 로드 시 로그 출력, 이벤트/커맨드 등 등록 |
서버 시작 시 정상 로드 확인 | ① Lifecycle |
| 블록 밟기 감지 | 플레이어 발 아래 블록을 감지 | ② Event Listener |
| /blockfade on/off로 기능 토글 | 기능을 켜고 끌 수 있는 명령어 | ③ Command |
| 바뀌는 블록 종류를 설정 파일로 지정 | config.yml에서 빨간 유리, 주황 유리 등 변경 | ④ Config |
| 밟은 후 1초 뒤에 블록 사라짐 | 밟으면 즉시 변하고, 1초 뒤에 제거 | ⑤ Scheduler |
| 사라진 블록 수를 화면에 표시 | 화면에 "사라진 블록: N개" 실시간 표시 | ⑥ Component |
2-2. 프로젝트 설정 및 구조

blockfade-plugin/
├── build.gradle
├── src/main/java/com/buildify365/blockfadePlugin/
│ ├── BlockFadePlugin.java ← 메인 클래스
│ ├── BlockFadeCommand.java ← 명령어 처리
│ └── BlockFadeListener.java ← 발판 감지 + 제거
└── src/main/resources/
├── plugin.yml
└── config.yml
2-3. plugin.yml + config.yml — 설정부터 깔기
plugin.yml:
name: BlockFade
version: 1.0.0
main: com.example.blockfade.BlockFadePlugin
api-version: '1.21'
commands:
blockfade:
description: 블록 사라짐 기능 토글
usage: /blockfade <on|off>
config.yml:
# 밟았을 때 바뀌는 블록 종류
fade-block: RED_STAINED_GLASS
기본값은 빨간 유리지만, MAGENTA_STAINED_GLASS, ORANGE_STAINED_GLASS 등으로 바꿀 수 있습니다.
코드를 수정할 필요 없이 config.yml만 고치면 됩니다. → ④ Config
2-4. 리스너 — Event + Scheduler + Component
핵심 기능부터 만듭니다. 블록을 밟으면 감지하고, 변하고, 1초 후 사라지는 로직입니다.
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.scheduler.BukkitScheduler;
import java.util.HashSet;
import java.util.Set;
public class BlockFadeListener implements Listener {
private final BlockFadePlugin plugin;
private final Set<Location> fadingBlocks = new HashSet<>();
public BlockFadeListener(BlockFadePlugin plugin) {
this.plugin = plugin;
}
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
if (!plugin.isFadeEnabled()) return; // 기능이 꺼져있으면 무시
Player player = event.getPlayer();
Block below = player.getLocation().subtract(0, 1, 0).getBlock();
if (below.getType() == Material.AIR) return;
if (fadingBlocks.contains(below.getLocation())) return;
fadingBlocks.add(below.getLocation());
// config.yml에서 바뀔 블록 종류 읽기
String blockName = plugin.getConfig().getString("fade-block", "RED_STAINED_GLASS");
Material fadeMaterial = Material.valueOf(blockName);
// 1단계: 즉시 설정된 블록으로 변경
below.setType(fadeMaterial);
// 2단계: 1초 후 블록 제거
BukkitScheduler scheduler = plugin.getServer().getScheduler();
scheduler.runTaskLater(plugin, () -> {
below.setType(Material.AIR);
fadingBlocks.remove(below.getLocation());
// 카운트 증가 + 화면에 표시
plugin.incrementFadeCount();
player.sendActionBar(
Component.text("사라진 블록: " + plugin.getFadeCount() + "개")
.color(NamedTextColor.YELLOW)
);
}, 20);
}
}
| 코드 | 사용된 개념 |
| @EventHandler + PlayerMoveEvent | ② Event Listener |
| plugin.getConfig().getString(...) | ④ Config |
| scheduler.runTaskLater(..., 20) | ⑤ Scheduler |
| player.sendActionBar(Component.text(...)) | ⑥ Component |
sendActionBar()란?
화면 하단 중앙(핫바 위)에 텍스트를 띄우는 기능입니다. 채팅창을 더럽히지 않고 실시간 정보를 보여줄 때 유용합니다.
2-5. 명령어 — Command
기능을 켜고 끄는 명령어를 만듭니다.
public class BlockFadeCommand implements CommandExecutor {
private final BlockFadePlugin plugin;
public BlockFadeCommand(BlockFadePlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0) {
sender.sendMessage(Component.text("사용법: /blockfade <on|off>")
.color(NamedTextColor.YELLOW));
return true;
}
switch (args[0].toLowerCase()) {
case "on" -> {
plugin.setFadeEnabled(true);
sender.sendMessage(Component.text("블록 사라짐 기능이 켜졌습니다!")
.color(NamedTextColor.GREEN));
}
case "off" -> {
plugin.setFadeEnabled(false);
sender.sendMessage(Component.text("블록 사라짐 기능이 꺼졌습니다!")
.color(NamedTextColor.RED));
}
default -> sender.sendMessage(Component.text("사용법: /blockfade <on|off>")
.color(NamedTextColor.YELLOW));
}
return true;
}
}
| 코드 | 사용된 개념 |
| implements CommandExecutor | ③ Command |
| Component.text(...).color(...) | ⑥ Component |
2-6. 메인 클래스 — 전부 취합
위에서 만든 리스너, 명령어, config를 한 곳에서 등록합니다.
public class BlockFadePlugin extends JavaPlugin {
private boolean enabled = false;
private int fadeCount = 0;
@Override
public void onEnable() {
saveDefaultConfig(); // config.yml 로드
getServer().getPluginManager().registerEvents(
new BlockFadeListener(this), this
);
getCommand("blockfade").setExecutor(new BlockFadeCommand(this));
getLogger().info("BlockFade 플러그인이 활성화되었습니다!");
}
@Override
public void onDisable() {
getLogger().info("BlockFade 플러그인이 비활성화되었습니다!");
}
public boolean isFadeEnabled() { return enabled; }
public void setFadeEnabled(boolean enabled) { this.enabled = enabled; }
public int getFadeCount() { return fadeCount; }
public void incrementFadeCount() { fadeCount++; }
}
| 코드 | 하는 일 |
| saveDefaultConfig() | ④ Config 로드 |
| registerEvents(new BlockFadeListener(...)) | 2-4에서 만든 리스너 등록 |
| setExecutor(new BlockFadeCommand(...)) | 2-5에서 만든 명령어 등록 |
| onEnable() / onDisable() | ① Lifecycle — 모든 준비와 정리를 여기서 |
2-7. 코드 흐름 정리
서버 시작
→ onEnable(): config 로드, 리스너·명령어 등록, 로그 출력 [① Lifecycle]
플레이어가 걸음
→ PlayerMoveEvent 발생 [② Event]
→ 기능 꺼져있으면? → 무시 [③ Command로 토글]
→ config에서 바뀔 블록 종류 읽기 [④ Config]
→ 해당 블록으로 변경 → 1초 후 AIR로 제거 [⑤ Scheduler]
→ "사라진 블록: N개" 액션바에 표시 [⑥ Component]
/blockfade off 입력
→ 기능 비활성화 → 블록이 더 이상 사라지지 않음 [③ Command]
2-8. 빌드 & 테스트
EP5에서 만든 개발 환경을 그대로 씁니다.
1. 빌드:
./gradlew build
2. 플러그인 복사:
build/libs/blockfade-1.0-SNAPSHOT.jar
→ C:\minecraft-test-server\plugins\
3. 서버 실행 → 접속 → 테스트:
테스트 항목확인
| 테스트 | 항목확인 |
| 서버 콘솔에 "활성화되었습니다!" 로그 | ① Lifecycle |
| 걸어다니면 블록이 변하고 사라짐 | ② Event + ⑤ Scheduler |
| /blockfade off → 블록 안 사라짐 | ③ Command |
| config.yml에서 블록 종류 변경 → 재시작 → 반영됨 | ④ Config |
| 화면 하단에 "사라진 블록: N개" 표시 | ⑥ Component |


마무리
이번 글에서 한 것
- Paper API 핵심 개념 — Lifecycle, Event, Command, Config, Scheduler, Component
- 6가지 전부 사용하는 미니 플러그인 제작 — 밟으면 블록이 사라지고, 토글/설정/연출까지
- 빌드 → 테스트
아직 게임이 아니다
블록이 사라지고, 명령어로 켜고 끌 수 있고, 카운트도 되지만 — 아직 "게임"은 아닙니다.
0장에서 보여드린 완성본을 만들려면 이 위에 게임 로직을 쌓아야 합니다:
| 필요한 기능 | 설명 |
| 아레나(경기장) 생성 | 블록으로 경기장 자동 생성 + 복원 |
| 참가/시작/종료 | 여러 명이 참가하고 게임을 진행하는 흐름 |
| 탈락/우승 판정 | 떨어지면 탈락, 마지막 1명이 우승 |
| 카운트다운, 우승 연출 | 3, 2, 1, START! + 우승 발표 |
다음 편에서 앞으로 어떤 기능이 필요한지, 어떻게 만들지 게임을 기획해봅니다.
그리고 현재 빌드된 플러그인을 복사해서 서버 플러그인 폴더에 옮기는 프로세스를 최적화 하는 방법도 추후에 한번 작성해보겠습니다.
References
- How do plugins work? — PaperMC Docs
- Event Listeners — PaperMC Docs
- Plugin Configurations — PaperMC Docs
- Scheduling — PaperMC Docs
- Paper API Javadoc
Javadocs
Find javadocs for our software – including Paper, Folia, Velocity, and Waterfall.
papermc.io
'게임' 카테고리의 다른 글
| 마인크래프트 플러그인 만들기 – 환경 세팅부터 배포까지 (IntelliJ + Paper API) | 마크빌드업 EP.05 (0) | 2026.02.24 |
|---|---|
| 마인크래프트 Paper 서버 구축 + 플러그인 설치 방법 (2026) | 마크빌드업 EP.04 (0) | 2026.02.23 |
| 마인크래프트 서버 도메인 연결 방법 (고정 IP + 무료 도메인) | 마크빌드업 EP.03 (0) | 2026.02.23 |
| 24시간 마인크래프트 서버 무료로 열기 – GCP 클라우드 활용 | 마크빌드업 EP.02 (0) | 2026.02.22 |
| 내 컴퓨터로 마인크래프트 서버 열기 (자바 에디션, 윈도우) | 마크빌드업 EP.01 (0) | 2026.02.20 |