2026. 2. 23. 23:11ㆍJava/마인크래프트
안녕하세요.
지난 2편에서 /heal 명령어를 만들면서 명령 처리, 권한, 인자 파싱, 탭 완성을 실전으로 다뤘습니다.
이번 3편에서는 Paper 플러그인에서 정말 자주 쓰는 핵심 기능인 이벤트 리스너(Event Listener) 를 실전으로 다뤄보겠습니다.
이번 목표는 간단하지만 서버 운영에 바로 도움이 되는 기능입니다.
- 스폰 주변 일정 반경을 보호구역으로 지정
- 일반 플레이어는 보호구역에서 블록 설치/파괴 불가
- 관리자 권한이 있으면 예외 허용
- 차단 이유를 플레이어에게 메시지로 안내
이 정도만 구현해도 "명령어 다음 단계"로 넘어가는 감각이 확실히 잡힙니다.
오늘 만들 기능 요약
기능명(예시): 스폰 보호구역 블록 보호
- 기준 위치: 월드 스폰(
world.getSpawnLocation()) - 보호 반경: 10칸 (코드 상수로 시작)
- 막는 행동:
- 블록 설치 (
BlockPlaceEvent) - 블록 파괴 (
BlockBreakEvent)
- 블록 설치 (
- 예외 대상:
paperstarter.spawnprotect.bypass권한이 있는 플레이어
먼저 알아둘 용어 (초보자 기준 설명)
event(이벤트): 서버 안에서 발생한 사건 (예: 블록 설치, 플레이어 접속, 데미지 발생)listener(리스너): 특정 이벤트가 발생했을 때 실행할 코드를 모아둔 클래스cancel(이벤트 취소): 원래 일어날 행동을 막는 것
예: 블록 설치 이벤트를 취소하면 실제로 블록이 설치되지 않음permission node(권한 문자열 이름): 권한 플러그인(LuckPerms 등)에서 관리하는 문자열 키
예:paperstarter.spawnprotect.bypass
plugin.yml 설정
한 줄 요약: 플러그인 기본 정보와 권한 노드를 등록합니다.
name: PaperStarter
version: 1.0.0
main: com.example.paperstarter.PaperStarterPlugin
api-version: '1.21'
permissions:
paperstarter.spawnprotect.bypass:
description: Allows bypassing spawn protection block restrictions
default: op
permissions에 권한 노드를 등록해두면, 나중에 운영할 때 권한 체계가 훨씬 깔끔해집니다.default: op로 두면 서버 OP는 자동으로 우회 권한을 가집니다.
메인 플러그인 클래스: 리스너 등록
한 줄 요약: 서버가 플러그인을 켤 때 이벤트 리스너를 등록합니다.
package com.example.paperstarter;
import org.bukkit.plugin.java.JavaPlugin;
public final class PaperStarterPlugin extends JavaPlugin {
@Override
public void onEnable() {
getServer().getPluginManager().registerEvents(new SpawnProtectListener(this), this);
getLogger().info("PaperStarter enabled");
}
@Override
public void onDisable() {
getLogger().info("PaperStarter disabled");
}
}
여기서 핵심은 registerEvents(...)입니다.
이 호출을 해야 서버가 "이 클래스가 이벤트를 처리하는 리스너구나"라고 인식하고, 실제 이벤트 발생 시 메서드를 호출합니다.
이벤트 리스너 클래스 구현
한 줄 요약: 블록 설치/파괴 이벤트를 받아서 보호구역이면 취소합니다.
package com.example.paperstarter;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
public final class SpawnProtectListener implements Listener {
private static final String BYPASS_PERMISSION = "paperstarter.spawnprotect.bypass";
private static final int SPAWN_PROTECT_RADIUS = 10;
private final PaperStarterPlugin plugin;
public SpawnProtectListener(PaperStarterPlugin plugin) {
this.plugin = plugin;
}
@EventHandler
public void onBlockPlace(BlockPlaceEvent event) {
Player player = event.getPlayer();
if (canBypass(player)) {
return;
}
if (isInsideSpawnProtection(event.getBlockPlaced().getLocation())) {
event.setCancelled(true);
player.sendMessage(ChatColor.RED + "스폰 보호구역에서는 블록을 설치할 수 없습니다.");
}
}
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
Player player = event.getPlayer();
if (canBypass(player)) {
return;
}
if (isInsideSpawnProtection(event.getBlock().getLocation())) {
event.setCancelled(true);
player.sendMessage(ChatColor.RED + "스폰 보호구역에서는 블록을 파괴할 수 없습니다.");
}
}
private boolean canBypass(Player player) {
return player.hasPermission(BYPASS_PERMISSION);
}
private boolean isInsideSpawnProtection(Location target) {
World world = target.getWorld();
if (world == null) {
return false;
}
Location spawn = world.getSpawnLocation();
if (!world.equals(spawn.getWorld())) {
return false;
}
double dx = target.getX() - spawn.getX();
double dz = target.getZ() - spawn.getZ();
return (dx * dx + dz * dz) <= (SPAWN_PROTECT_RADIUS * SPAWN_PROTECT_RADIUS);
}
}
핵심 포인트는 3가지입니다.
@EventHandler: 이 메서드가 이벤트 처리 메서드라는 표시event.setCancelled(true): 실제 행동(설치/파괴)을 막는 핵심 코드hasPermission(...): 관리자/운영자 예외 처리를 위해 꼭 필요한 분기
왜 이 줄이 중요한가? (실전 포인트)
hasPermission(...)를 왜 넣나?
서버 운영에서는 "모두 금지"보다 "일반 유저만 금지, 운영자는 가능"이 거의 항상 필요합니다.
이 분기가 없으면 운영자가 직접 스폰 정리/수정을 할 때도 막혀서 불편해집니다.
반경 계산을 왜 distance() 대신 직접 계산했나?
distance()는 내부적으로 제곱근 계산이 들어가서, 이벤트가 매우 자주 발생하는 상황에서는 불필요한 비용이 생길 수 있습니다.
여기서는 dx*dx + dz*dz 비교로 충분해서 더 가볍게 처리했습니다.
왜 Y축은 무시했나?
스폰 보호는 보통 "수평 범위(XZ)" 기준으로 운영하는 경우가 많습니다.
처음에는 단순하게 만들고, 필요하면 나중에 Y축 제한을 추가하는 게 유지보수에 좋습니다.
자주 나오는 실수 (실패 케이스)
1) registerEvents(...)를 안 한 경우
증상:
- 코드가 맞아 보이는데 아무 것도 막히지 않음
원인:
- 리스너 클래스는 만들었지만 서버에 등록하지 않아서 이벤트가 호출되지 않음
2) @EventHandler를 빼먹은 경우
증상:
- 메서드는 있는데 이벤트가 실행되지 않음
원인:
- 리스너 메서드 표시가 없어서 Bukkit/Paper가 이벤트 핸들러로 인식하지 않음
3) 권한 노드 오타
증상:
- OP가 아닌데 우회되거나, OP인데도 막힘(권한 플러그인 설정에 따라 혼동)
원인:
- 코드 문자열과
plugin.yml권한 노드 이름이 다름
수동 테스트 체크리스트 (서버 운영 관점)
- 서버 기동 후 에러 없이 플러그인이 enable 되는지 확인
- 일반 플레이어로 스폰 근처에서 블록 설치 시 차단되는지 확인
- 일반 플레이어로 스폰 근처에서 블록 파괴 시 차단되는지 확인
- 차단 메시지가 이해 가능하게 출력되는지 확인
- 스폰 보호 반경 밖에서는 정상 설치/파괴 되는지 확인
- OP(또는 우회 권한 보유 계정)는 스폰 안에서도 작업 가능한지 확인
이번 편 정리
이번 편에서 한 일은 단순하지만, 실제 플러그인 개발에서 매우 중요한 패턴입니다.
- 이벤트를 듣고 (
Listener) - 조건을 검사하고
- 필요하면 취소한다 (
setCancelled(true))
이 패턴은 이후에도 그대로 반복됩니다.
예를 들어 다음에는 이런 것들로 확장할 수 있습니다.
- 특정 월드에서만 보호 적용
- 특정 블록만 설치 금지
- 시간대/게임모드별 예외 처리
- 설정파일(
config.yml)로 반경 변경 가능하게 만들기
'Java > 마인크래프트' 카테고리의 다른 글
| [Paper 플러그인 실전 제작기 #05] 데이터 저장 1: YAML로 유저별 설정 저장 (0) | 2026.02.26 |
|---|---|
| [Paper 플러그인 실전 제작기 #04] config.yml로 기능 on/off + 메시지 커스터마이징 (2) | 2026.02.25 |
| [Paper 플러그인 실전 제작기 #02] 명령어 시스템 실전 (/heal 권한, 인자 파싱, 탭 완성) (0) | 2026.02.21 |
| [Paper 플러그인 실전 제작기 #01] Java 21 + Maven으로 첫 플러그인 만들기 (0) | 2026.02.20 |
| Paper 플러그인 실전 제작기 (0) | 2026.02.20 |