튜토리얼/마인크래프트 플러그인

[Minecraft Plugin Tutorial] 2. 간단한 슈팅 게임 제작

2023. 4. 14. 22:55
목차
  1. 시작하기에 앞서
  2. 프로젝트 목표
  3. 1. 게임 관리자 제작
  4. 2. 게임 커맨드 제작
  5. 3. 스코어 리스너 제작
  6. 마치며

시작하기에 앞서

아직 개발 환경을 설정하지 못했다면 이전 포스트를 참고해 주세요.

 

이번 시간에는 다음 항목을 학습하는 것을 목표로 합니다.

  • 전체 메세지와 플레이어 메시지를 전송하는 방법
  • 커맨드를 제작하고 등록하는 방법
  • 이벤트 리스너를 제작하고 등록하는 방법

프로젝트 목표

플레이어가 화살을 쏘아 상대 플레이어를 맞추거나 처치했을 때 스코어가 오르는 게임을 제작합니다.

해당 게임을 구현하기 위해 다음과 같은 항목을 필요로 합니다.

  • 게임 시작/종료 기능
  • 플레이어의 스코어 정보를 저장하는 기능
  • 조건 만족 시 플레이어의 스코어를 변경하는 기능

결과물은 다음과 같습니다.

 

전체 코드는 깃허브에서 볼 수 있습니다.

 


1. 게임 관리자 제작

먼저, 게임의 진행을 관리할 수 있는 관리자 클래스를 제작합니다.

관리자 클래스에서는 게임의 진행 상태 및 플레이어의 스코어 정보를 관리합니다.

게임 관리자는 단 하나의 인스턴스만 생성되기를 바라기 때문에 싱글톤 클래스로 제작합니다.

public class GameController {

    // 인스턴스를 단 하나만 생성하기 위해 Holder클래스를 제작합니다.
    private static class GameControllerHolder {
        private static final GameController INSTANCE = new GameController();
    }

    // Holder클래스에서 생성된 인스턴스를 반환합니다.
    public static GameController getInstance() {
        return GameControllerHolder.INSTANCE;
    }

    // 게임의 진행 상태를 저장합니다.
    private boolean isPlaying = false;
    
    // 플레이어의 스코어 정보를 저장합니다.
    private Map<String, Integer> scores = Maps.newHashMap();

    // 직접적으로 객체를 생성할 수 없도록 private으로 처리합니다.
    private GameController() {}

    // 게임을 시작합니다.
    public void startGame() {
        if (!isPlaying) { // 게임이 실행중이지 않을 때
            isPlaying = true; // 게임을 시작 상태로 변경합니다.
            scores = Maps.newHashMap(); // 플레이어의 스코어 정보를 초기화 합니다.
        }
    }

    // 게임을 종료합니다.
    public void stopGame() {
        if (isPlaying) { // 게임이 실행중일 때
            isPlaying = false; // 게임을 종료 상태로 변경합니다.
        }
    }

    // 플레이어의 스코어를 설정합니다.
    public void setScore(@NotNull Player player, int score) {
        if (isPlaying) { // 게임이 실행중일 때
            // 플레이어의 UUID를 가져옵니다.
            final String uuid = player.getUniqueId().toString();

            // 플레이어의 스코어를 변경합니다.
            scores.put(uuid, score);
        }
    }

    // 플레이어의 스코어 정보를 반환합니다.
    public int getScore(@NotNull Player player) {
    	// 플레이어의 UUID를 가져옵니다.
        final String uuid = player.getUniqueId().toString();
        
        // 플레이어의 스코어 정보를 조회하고
        // 스코어가 존재한다면 가져온 스코어를, 그렇지 않다면 0을 반환합니다.
        return scores.getOrDefault(uuid, 0);
    }

    // 모든 플레이어의 스코어를 반환합니다.
    public Map<String, Integer> getScores() {
        return Maps.newHashMap(scores);
    }

    // 게임이 진행중이면 true, 그렇지 않다면 false를 반환합니다.
    public boolean isPlaying() {
        return isPlaying;
    }

}
관련 코드는 깃허브에서도 확인할 수 있습니다.

 


2. 게임 커맨드 제작

다음으로 게임을 시작하고 종료할 수 있는 커맨드를 제작합니다.

커맨드의 자동 완성 기능을 제작합니다.

CommandExecutor 인터페이스를 구현하여 커맨드 입력 시 작동할 기능을 작성합니다.

TabCompleter 인터페이스를 구현하여 커맨드 자동 완성 기능을 구현합니다.

 

먼저, 커맨드 클래스를 제작합니다.

// CommandExecutor: onCommand() 메소드를 구현하여 커맨드의 작동 방식을 구현합니다.
// TabCompleter: onTabComplete() 메소드를 구현하여 커맨드의 자동 완성을 구현합니다.
public class GameCommand implements CommandExecutor, TabCompleter {

    @Override
    public boolean onCommand(
    	@NotNull CommandSender sender, @NotNull Command command, 
        @NotNull String label, @NotNull String[] args
    ) {
    
        // 입력한 커맨드 인자의 길이가 0이라면 종료합니다.
        // /shoot <- 인자의 길이가 0인 커맨드 입니다.
        // /shoot start <- 인자의 길이가 1인 커맨드 입니다.
        if (args.length == 0) {
            return false;
        }

        // 가장 처음으로 입력받은 인자가 무엇인지 확인합니다.
        switch (args[0]) {
            // /shoot start 커맨드를 입력하였을 때 실행됩니다.
            case "start" -> {
            	// GameController 객체를 가져옵니다.
                GameController.getInstance().startGame();
                
                // Bukkit.broadcast() 메소드를 사용하여 모든 유저에게 메시지를 전송합니다.
                // 또한, Paper 에서는 문자열 처리를 String 대신 Component를 사용하는것을 권장합니다.
                Bukkit.broadcast(Component.text("Start Game."));
            }
            // /shoot stop 커맨드를 입력하였을 때 실행됩니다.
            case "stop" -> {
            	// GameController 객체를 가져옵니다.
                GameController.getInstance().stopGame();
                
                // 모든 유저에게 메세지를 전송합니다.
                Bukkit.broadcast(Component.text("Stop Game."));
            }
        }

        return false;
    }

    @Override
    public @Nullable List<String> onTabComplete(
    	@NotNull CommandSender sender, @NotNull Command command, 
        @NotNull String label, @NotNull String[] args
    ) {
        // 입력한 커맨드 인자의 길이가 1이 아니라면 종료합니다.
        if (args.length != 1) {
            return null;
        }

        // 첫번째 인자의 자동완성을 "start" 와 "stop" 으로 설정합니다.
        return Lists.newArrayList("start", "stop");
    }

}
관련 코드는 깃허브에서도 확인할 수 있습니다.

 

다음으로, plugin.yml 파일에 커맨드 정보를 입력합니다.

name: ShootingGame
version: '${version}'
main: com.molruexception.shootinggame.ShootingGame
api-version: 1.19

# 등록할 커맨드 목록
commands:
  # 등록할 커맨드 이름 /shoot <arg 1>
  shoot:
    # 커맨드를 사용할 때 필요한 퍼미션
    permission: game.command.shoot
관련 코드는 깃허브에서도 확인할 수 있습니다.

 

마지막으로, 제작한 커맨드를 메인 클래스의 onEnable() 메소드에서 등록합니다.

public final class ShootingGame extends JavaPlugin {

    @Override
    public void onEnable() {
        // plugin.yml에 작성한 커맨드를 불러옵니다.
        final PluginCommand shootCommand = getCommand("shoot");
        
        // 커맨드를 정상적으로 불러왔다면
        if (shootCommand != null) {
            // 커맨드 객체를 생성합니다.
            GameCommand command = new GameCommand();
            
            // 불러온 커맨드의 실행 객체를 위에서 생성한 커맨드 객체로 설정합니다.
            shootCommand.setExecutor(command);
            
            // 불러온 커맨드의 자동완성 객체를 위에서 생성한 커맨드 객체로 설정합니다.
            shootCommand.setTabCompleter(command);
        }
    }

}
관련 코드는 깃허브에서도 확인할 수 있습니다.

 


3. 스코어 리스너 제작

먼저, 리스너 클래스를 제작합니다.

리스너 클래스는 Listener 인터페이스를 구현하여 이벤트 리스너를 추가합니다.

// Listener 인터페이스를 구현하여 이벤트 리스너를 추가합니다.
public class GameListener implements Listener {

    // 화살 적중시 스코어를 5점으로 설정합니다.
    private static final int ARROW_HIT_SCORE = 5;
    
    // 적 처치시 스코어를 20점으로 설정합니다.
    private static final int PLAYER_KILL_SCORE = 20;

    // EventHandler 어노테이션을 부착하여 이벤트를 수신합니다.
    @EventHandler
    public void onArrowHit(ProjectileHitEvent event) { // ProjecttileHitEvent에서 발사체 피격을 감지합니다.
        // 게임이 실행중인지 확인하고 실행중이지 않다면 종료합니다.
        final GameController controller = GameController.getInstance();
        if (!controller.isPlaying()) {
            return;
        }

        // 투사체를 피격한 엔티티가 플레이어가 아니라면 종료합니다.
        if (!(event.getHitEntity() instanceof Player)) {
            return;
        }

        // 투사체가 화살이 아니라면 종료합니다.
        if (!(event.getEntity() instanceof Arrow arrow)) {
            return;
        }

        // 화살을 날린 주체가 플레이어가 아니라면 종료합니다.
        if (!(arrow.getShooter() instanceof Player shooter)) {
            return;
        }

        // 화살을 날린 플레이어의 스코어를 증가시킵니다.
        final int score = controller.getScore(shooter) + ARROW_HIT_SCORE;
        controller.setScore(shooter, score);

        // Player#sendMessage() 메소드를 사용하여 플레이어에게 메시지를 전송합니다.
        // Paper 에서는 String 대신 Component 사용을 권장합니다.
        shooter.sendMessage(Component.text(String.format(
                "적을 공격하여 %d 포인트를 획득하였습니다. 현재 스코어: %d",
                ARROW_HIT_SCORE, score
        )));
    }

    // EntityDeathEvent 에서 엔티티의 사망을 감지합니다.
    @EventHandler
    public void onDeath(EntityDeathEvent event) {
    
        // 게임이 실행중인지 확인하고 실행중이지 않다면 종료합니다.
        final GameController controller = GameController.getInstance();
        if (!controller.isPlaying()) {
            return;
        }

        // 사망한 엔티티가 플레이어라면 victim 으로 캐스팅하고
        // 그렇지 않다면 종료합니다.
        if (!(event.getEntity() instanceof Player victim)) {
            return;
        }

        // 사망한 플레이어를 누가 죽였는지 불러옵니다.
        final Player killer = victim.getKiller();
        
        // 공격한 플레이어를 불러올 수 있다면
        if (killer != null) {
        
            // 공격한 플레이어의 스코어를 증가시킵니다.
            final int score = controller.getScore(killer) + PLAYER_KILL_SCORE;
            controller.setScore(killer, score);

            // Player#sendMessage() 메소드를 사용하여 메시지를 전송합니다.
            // Paper 에서는 String 대신 Component 사용을 권장합니다.
            killer.sendMessage(Component.text(String.format(
                    "적을 처치하여 %d 포인트를 획득하였습니다. 현재 스코어: %d",
                    PLAYER_KILL_SCORE, score
            )));
        }
    }

}
관련 코드는 깃허브에서도 확인할 수 있습니다.

 

다음으로, 제작한 리스너를 메인 클래스의 onEnable() 메소드에서 등록합니다.

public final class ShootingGame extends JavaPlugin {

    @Override
    public void onEnable() {
        // Register Command
        final PluginCommand shootCommand = getCommand("shoot");
        if (shootCommand != null) {
            GameCommand command = new GameCommand();
            shootCommand.setExecutor(command);
            shootCommand.setTabCompleter(command);
        }

        // 플러그인 매니저를 불러옵니다.
        final PluginManager pm = Bukkit.getPluginManager();
        
        // 이벤트 리스너 객체를 생성하여 등록합니다.
        pm.registerEvents(new GameListener(), this);
    }

}
관련 코드는 깃허브에서도 확인할 수 있습니다.

 


마치며

이해가 되지 않는 부분이나 궁금하신 점이 있다면 댓글로 남겨주세요.

 

'튜토리얼 > 마인크래프트 플러그인' 카테고리의 다른 글

[Minecraft Plugin Tutorial] 1. 플러그인 개발환경 설정  (0) 2023.04.14
  1. 시작하기에 앞서
  2. 프로젝트 목표
  3. 1. 게임 관리자 제작
  4. 2. 게임 커맨드 제작
  5. 3. 스코어 리스너 제작
  6. 마치며
'튜토리얼/마인크래프트 플러그인' 카테고리의 다른 글
  • [Minecraft Plugin Tutorial] 1. 플러그인 개발환경 설정
Lede_
Lede_
개발 일지, 아이디어 등 Lede의 기록을 수집하고 기록하는 공간
Lede's Archive개발 일지, 아이디어 등 Lede의 기록을 수집하고 기록하는 공간
반응형
Lede_
Lede's Archive
Lede_
전체
오늘
어제
  • 전체보기 (32)
    • 마인크래프트 (9)
      • 스크립트 (8)
      • 플러그인 (1)
    • Unreal Engine (7)
      • 팁 (7)
      • Material (0)
    • 공부합시다 (9)
      • DesignPattern (4)
      • Java, Android (5)
    • 책 리뷰 (1)
      • Clean Code (1)
    • 프로젝트 (0)
    • 튜토리얼 (6)
      • 마인크래프트 스크립트 (4)
      • 마인크래프트 플러그인 (2)
hELLO · Designed By 정상우.
Lede_
[Minecraft Plugin Tutorial] 2. 간단한 슈팅 게임 제작
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.