플레이어 이벤트
PlayerLoggedInEvent, LivingHurtEvent, LivingDeathEvent 등 주요 플레이어/Living 이벤트를 구현하고, 사이드 분기 처리로 클라이언트-서버 동기화 문제를 예방합니다.
플레이어와 관련된 이벤트는 NeoForge 모딩에서 가장 자주 쓰는 카테고리입니다. 접속 환영 메시지부터 데미지 조정, 사망 처리까지 게임플레이의 핵심 흐름을 제어할 수 있습니다.
이 챕터에서는 네 가지 핵심 이벤트를 직접 구현하고, 양 사이드(클라이언트/서버)에서 호출되는 이벤트를 올바르게 처리하는 방법을 배웁니다.
이벤트 클래스 준비
모든 플레이어 이벤트 핸들러를 하나의 클래스에 모읍니다. @EventBusSubscriber로 자동 등록합니다.
package com.example.examplemod.events;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.entity.living.LivingDeathEvent;
import net.neoforged.neoforge.event.entity.living.LivingHurtEvent;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
@EventBusSubscriber(modid = "examplemod")
public class PlayerEventHandler {
// 핸들러 메서드들이 여기 들어갑니다
}@EventBusSubscriber는 클래스 내 static 핸들러를 자동으로 NeoForge 이벤트 버스에 등록합니다. modid는 반드시 mods.toml의 mod ID와 일치해야 합니다.
1. PlayerLoggedInEvent — 접속 환영 메시지
플레이어가 서버에 접속할 때 발생합니다. 환영 메시지, 초기 아이템 지급, 통계 초기화 등에 활용합니다.
@SubscribeEvent
public static void onLogin(PlayerEvent.PlayerLoggedInEvent event) {
if (event.getEntity() instanceof ServerPlayer player) {
player.sendSystemMessage(
Component.literal("§a환영합니다, " + player.getName().getString() + "!")
);
}
}instanceof ServerPlayer 패턴 매칭으로 서버 플레이어임을 확인합니다. PlayerLoggedInEvent는 서버에서만 발생하지만, 명시적 타입 확인으로 의도를 분명히 합니다.
§a는 초록색 텍스트 색상 코드입니다. 더 복잡한 스타일링이 필요하면 Component.literal(...).withStyle(ChatFormatting.GREEN) 방식을 권장합니다.
📷 스크린샷 자리 (직접 캡처해 추가하세요)
플레이어 접속 시 환영 메시지가 채팅창에 표시된 화면
2. LivingHurtEvent — 데미지 감쇄
엔티티가 피해를 받을 때 발생합니다. Cancelable 인터페이스를 구현하므로 취소하거나 데미지 양을 조정할 수 있습니다.
@SubscribeEvent
public static void onHurt(LivingHurtEvent event) {
if (!event.getEntity().level().isClientSide()) {
if (event.getEntity() instanceof Player) {
event.setAmount(event.getAmount() * 0.5f); // 데미지 50% 감소
}
}
}isClientSide() 가드가 핵심입니다. LivingHurtEvent는 클라이언트와 서버 양쪽에서 호출됩니다. 데미지 수치 변경은 서버 권위 데이터이므로 서버에서만 처리해야 합니다.
⚠️ 사이드 분기 누락 — 이중 적용
// ❌ 양 사이드에서 데미지 감쇄 → 클라+서버 동기화 깨짐 public static void onHurt(LivingHurtEvent event) { event.setAmount(event.getAmount() * 0.5f); // 가드 없이 } // ✅ 서버에서만 public static void onHurt(LivingHurtEvent event) { if (!event.getEntity().level().isClientSide()) { event.setAmount(event.getAmount() * 0.5f); } }데미지/상태 변경은 서버 권위입니다. 클라이언트는 동기화만 담당합니다. 가드 없이 양 사이드에서 실행하면 클라이언트 예측값과 서버 실제값이 어긋나 체력 표시 버그가 발생합니다.
이벤트 취소
데미지를 완전히 막으려면 setCanceled(true)를 사용합니다.
@SubscribeEvent
public static void onHurt(LivingHurtEvent event) {
if (!event.getEntity().level().isClientSide()) {
if (event.getEntity() instanceof Player player) {
// 특정 조건에서 무적 처리
if (player.hasEffect(MobEffects.ABSORPTION)) {
event.setCanceled(true);
}
}
}
}LivingHurtEvent는 ICancellableEvent를 구현합니다. 취소하면 데미지 계산 파이프라인 전체가 중단됩니다.
3. LivingDeathEvent — 사망 처리
엔티티가 사망할 때 발생합니다. 사망 직전 상태이므로 아이템 지급, 통계 기록, 특수 리스폰 로직 등을 처리할 수 있습니다.
@SubscribeEvent
public static void onDeath(LivingDeathEvent event) {
if (!event.getEntity().level().isClientSide()) {
if (event.getEntity() instanceof ServerPlayer player) {
// 사망 시 부활 보너스 아이템 지급
player.getInventory().add(
new ItemStack(Items.GOLDEN_APPLE)
);
}
}
}LivingDeathEvent도 양 사이드에서 발생합니다. 인벤토리 조작은 서버에서만 해야 합니다.
이 이벤트는 취소 가능합니다. event.setCanceled(true)로 사망 자체를 막을 수 있지만, 체력이 0 이하인 상태가 유지되므로 즉시 체력을 회복시켜야 합니다.
@SubscribeEvent
public static void onDeath(LivingDeathEvent event) {
if (!event.getEntity().level().isClientSide()) {
if (event.getEntity() instanceof ServerPlayer player) {
// 사망 취소 + 체력 회복 (부활 효과)
event.setCanceled(true);
player.setHealth(1.0f);
}
}
}4. PlayerInteractEvent.RightClickItem — 아이템 우클릭
플레이어가 아이템을 우클릭할 때 발생합니다. 아이템 자체의 use() 메서드보다 먼저 호출되므로 기존 동작을 가로채거나 조건부로 막을 수 있습니다.
@SubscribeEvent
public static void onRightClickItem(PlayerInteractEvent.RightClickItem event) {
if (!event.getEntity().level().isClientSide()) {
Player player = event.getEntity();
ItemStack stack = event.getItemStack();
if (stack.is(Items.STICK)) {
player.sendSystemMessage(
Component.literal("막대기를 우클릭했습니다!")
);
// 기본 동작을 막으려면:
// event.setCanceled(true);
}
}
}PlayerInteractEvent는 여러 하위 이벤트로 나뉩니다.
| 이벤트 | 발생 시점 |
|---|---|
RightClickItem | 아이템 우클릭 (공중) |
RightClickBlock | 블록 우클릭 |
LeftClickBlock | 블록 좌클릭 (채굴 시작) |
EntityInteract | 엔티티 우클릭 |
사이드 확인 요약
이벤트별 사이드 호출 여부를 정리합니다.
| 이벤트 | 클라이언트 | 서버 | 권장 처리 |
|---|---|---|---|
PlayerLoggedInEvent | ❌ | ✅ | 서버 전용 — 가드 불필요 |
LivingHurtEvent | ✅ | ✅ | isClientSide() 가드 필수 |
LivingDeathEvent | ✅ | ✅ | isClientSide() 가드 필수 |
RightClickItem | ✅ | ✅ | isClientSide() 가드 필수 |
서버 전용 이벤트라도 instanceof ServerPlayer 패턴 매칭으로 의도를 명확히 하는 습관을 들이세요. 코드 리뷰 시 사이드 처리 의도가 바로 보입니다.
전체 핸들러 클래스
지금까지 구현한 핸들러를 하나로 모읍니다.
package com.example.examplemod.events;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.entity.living.LivingDeathEvent;
import net.neoforged.neoforge.event.entity.living.LivingHurtEvent;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
@EventBusSubscriber(modid = "examplemod")
public class PlayerEventHandler {
@SubscribeEvent
public static void onLogin(PlayerEvent.PlayerLoggedInEvent event) {
if (event.getEntity() instanceof ServerPlayer player) {
player.sendSystemMessage(
Component.literal("§a환영합니다, " + player.getName().getString() + "!")
);
}
}
@SubscribeEvent
public static void onHurt(LivingHurtEvent event) {
if (!event.getEntity().level().isClientSide()) {
if (event.getEntity() instanceof Player) {
event.setAmount(event.getAmount() * 0.5f);
}
}
}
@SubscribeEvent
public static void onDeath(LivingDeathEvent event) {
if (!event.getEntity().level().isClientSide()) {
if (event.getEntity() instanceof ServerPlayer player) {
player.getInventory().add(new ItemStack(Items.GOLDEN_APPLE));
}
}
}
@SubscribeEvent
public static void onRightClickItem(PlayerInteractEvent.RightClickItem event) {
if (!event.getEntity().level().isClientSide()) {
Player player = event.getEntity();
ItemStack stack = event.getItemStack();
if (stack.is(Items.STICK)) {
player.sendSystemMessage(
Component.literal("막대기를 우클릭했습니다!")
);
}
}
}
}다음 챕터
다음 챕터에서는 블록과 월드 관련 이벤트를 다룹니다. 블록 파괴, 청크 로드, 날씨 변경 등 월드 레벨 이벤트를 처리하는 방법을 배웁니다.