개인 자료란 (JE)

  서버 커뮤니티


Profile 바뮤 대표칭호 없음

wbm2 ae5f28970b944aeb94a1e2f8ed4a9544

Profile

강좌 및 개발 자바 에디션(JE) 플러그인 개발

[튜토리얼] 미니게임월드: Hungry Fishing

2022.06.04 조회 수 328 추천 수 0
분야 플러그인 
장르 개발자 툴 
게임버전 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x, 1.19.x 
API 버킷, 스피곳, 페이퍼, 퍼퍼 

시작 전

만약 처음이시라면 API 소개세팅 가이드 를 읽어서 초반 세팅을 할 수 있습니다


Hungry Fishing

최소 0.8.1 API가 필요한 튜토리얼 입니다

안녕하세요 여러분, 이번에는 개인전(솔로 배틀) 미니게임 튜토리얼로 돌아왔습니다

우리가 만들 미니게임은 배고픈 낚시 입니다. 규칙은 간단합니다. 플레이어가 낚시를 해서 잡은 아이템으로 배고픔을 모두 채우는 게임입니다

이번 튜토리얼에는 추가적으로 커스텀 데이터 도 다루는 방법을 알아보겠습니다

1. 규칙

  1. 만약 플레이어가 자신의 배고픔을 모두 채웠다면, 남은 플레이 시간 만큼 점수를 얻고, 관전자 모드로 변경 됩니다
  2. 낚시로 아이템이 낚였을 때, 우리가 설정해줄 커스텀 데이터에 있는 아이템 리스트의 확률에 따른 아이템으로 변경해줍니다
  3. 만약 플레이어가 낚시를 실패했다면, 일정 배고픔을 감소시킵니다

2. 클래스 만들기

HungryFishing클래스를 패키지 안에 만든후 SoloBattleMiniGame클래스를 상속합니다 (생성자는 super()에 직접 값을 넘겨주게 아래처럼 설정해주세요)

public class HungryFishing extends SoloBattleMiniGame {

    public HungryFishing() {
        super("HungryFishing", 2, 10, 60 * 5, 20);
    }

    @Override
    protected void initGame() {
    }

    @Override
    protected void onEvent(Event event) {
    }

    @Override
    protected List<String> tutorial() {
        return null;
    }
}

메뉴 아이콘과 추가적인 설정들도 같이 해줍니다

public HungryFishing() {
  super("HungryFishing", 2, 10, 60 * 5, 20);

  // 세팅
  getSetting().setIcon(Material.FISHING_ROD);

  // 옵션
  getCustomOption().set(Option.COLOR, ChatColor.AQUA);
  getCustomOption().set(Option.PLAYER_HURT, false);
  getCustomOption().set(Option.PVP, false);
  getCustomOption().set(Option.PVE, false);
}

3. 커스텀 데이터 설정하기

이제 처음 배워보는 커스텀 데이터들을 다뤄볼 시간입니다

먼저 데이터를 사용하려면 변수가 필요하므로, 아래처럼 변수를 선언해주세요

public class HungryFishing extends SoloBattleMiniGame {

    private Map<Material, Integer> catchItems;
    private int hunger;
    private int failHunger;
    ...
}

그 다음, initCustomData()와 loadCustomData() 메서드를 오버라이드 해주세요

@Override
protected void initCustomData() {
    super.initCustomData();
}

@Override
public void loadCustomData() {
    super.loadCustomData();
}

initCustomData() 메서드에서는 커스텀 데이터들을 초기화(직렬화)해주는 곳입니다

  • cath-items: 플레이어가 물고기를 잡았을 때 확률로 설정되는 아이템 리스트 데이터
  • hunger: 게임이 시작될 때 플레이어의 초기 배고픔 수치
  • fail-hunger: 플레이어가 낚시를 실패했을 때 감소되는 배고픔 수치
@Override
protected void initCustomData() {
    super.initCustomData();

    Map<String, Object> data = getCustomData();

    // 잡히는 아이템
    Map<String, Integer> itemList = new HashMap<>();
    itemList.put(Material.COOKIE.name(), 40); // 쿠키: 40% 확률
    itemList.put(Material.MELON_SLICE.name(), 30); // 수박: 30% 확률
    itemList.put(Material.CARROT.name(), 20); // 당근: 20% 확률
    itemList.put(Material.COOKED_PORKCHOP.name(), 10); // 구운 돼지고기: 10% 확률
    data.put("catch-items", itemList);

    // 배고픔
    data.put("hunger", 1);

    // 실패-배고픔
    data.put("fail-hunger", 2);
}

loadCustomData()메서드에서는 yaml파일에 저장된 데이터를 로드(역직렬화)해줍니다

@Override
public void loadCustomData() {
    super.loadCustomData();

    Map<String, Object> data = getCustomData();

    // 잡히는 아이템
    this.catchItems = new HashMap<>();
    Map<String, Integer> itemList = (Map<String, Integer>) data.get("catch-items");
    itemList.forEach((k, v) -> catchItems.put(Material.valueOf(k), v));

    // 배고픔
    this.hunger = (int) data.get("hunger");

    // 실패-배고픔
    this.failHunger = (int) data.get("fail-hunger");
}

그리고 나중에 사용할 확률에 맞게 catch-items에서 나오는 랜덤한 아이템을 얻을 수 있는 메서드를 만들어줍니다

Material getRandomItem() {
    int random = new Random().nextInt(100);

    int range = 0;
    for (Entry<Material, Integer> entry : this.catchItems.entrySet()) {
        Material item = entry.getKey();
        int percent = entry.getValue();

        range += percent;

        if (random < range) {
            return item;
        }
    }

    // 안전
    return Material.COOKIE;
}

4. 이벤트 처리하기

우리는 물고기를 잡았을 때를 감지하기 위해 PlayerFishEvent가 필요하고, 플레이어가 배고픔을 채웠을 때를 감지하기 위해 FoodLevelChangeEvent가 필요합니다

4.1 PlayerFishEvent

먼저 onFishEvent()메서드로 간단하게 연결해줍니다

@Override
protected void onEvent(Event event) {
    if (event instanceof PlayerFishEvent) {
        onFishEvent((PlayerFishEvent) event);
    }
}

void onFishEvent(PlayerFishEvent event) {
}

이 이벤트에서는 물고기 잡기와 실패만 처리를 해줍니다

그 다음 onCatch()와 onFail()메서드도 만들어서 각 상황에 맞게 연결해줍니다

void onFishEvent(PlayerFishEvent event) {
    State state = event.getState();

    if (state == State.CAUGHT_FISH) {
        onCatch(event);
    } else if (state == State.FAILED_ATTEMPT) {
        onFail(event);
    }
}

void onCatch(PlayerFishEvent event) {
}

void onFail(PlayerFishEvent event) {
}

onCatch()메서드에서는 잡힌 물고기를 우리가 전에 만든 getRandomItem()를 이용해서 바꿔치기 해줍니다 (추가로 메세지같은 부가 작업을 해줍니다)

void onCatch(PlayerFishEvent event) {
    Player p = event.getPlayer();

    Entity entity = event.getCaught();
    ItemStack item = ((Item) entity).getItemStack();

    // 잡힌 아이템을 랜덤 잡히는 아이템으로 설정
    item.setType(getRandomItem());

    // 메세지, 타이틀 보내기
    sendMessages(p.getName() + ChatColor.GREEN + "가 물고기 낚았습니다!");
    sendTitle(p, ChatColor.GREEN + "잡았다!", "");

    // 소리
    p.playSound(p.getLocation(), Sound.BLOCK_BELL_USE, 10.0F, 1.0F);
}

onFail()에서는 실패했을 때이므로, 플레이어의 배고픔을 fail-hunger만큼 감소시켜줍니다 (추가로 메세지같은 부가 작업을 해줍니다)

void onFail(PlayerFishEvent event) {
    Player p = event.getPlayer();

    // 플레이어 배고픔 실패-배고픔만큼 감소시키기
    int playerHunger = p.getFoodLevel() - this.failHunger;
    if (playerHunger < 1) {
        playerHunger = 1;
    }
    p.setFoodLevel(playerHunger);

    // 메세지, 타이틀 보내기
    sendMessages(p.getName() + ChatColor.RED + " 가 물고기를 낚는데 실패했습니다!");
    sendMessage(p, "당신은 " + this.failHunger + " 만큼 배고픔을 잃었습니다");
    sendTitle(p, ChatColor.RED + "물고기 도망쳤다!", "");

    // 소리
    p.playSound(p.getLocation(), Sound.BLOCK_ANVIL_DESTROY, 10.0F, 1.0F);
}

4.2 FoodLevelChangeEvent

플레이어가 음식을 먹을 때 우리는 배고픔을 감지 할 수 있으므로 onFoodLevelChange()메서드를 만들어서 연결해줍니다

@Override
protected void onEvent(Event event) {
    if (event instanceof PlayerFishEvent) {
        onFishEvent((PlayerFishEvent) event);
    } else if (event instanceof FoodLevelChangeEvent) {
        onFoodLevelChange((FoodLevelChangeEvent) event);
    }
}

void onFoodLevelChange(FoodLevelChangeEvent event) {
}

onFoodLevelChange()에서 확인할것은 2가지 입니다

  1. 플레이어의 배고픔이 꽉 찻는지 검사
  2. 만약 꽉 찻다면, 모든 플레이어가 꽉 찻는지 검사 (종료 시점 확인)
void onFoodLevelChange(FoodLevelChangeEvent event) {
    Player p = (Player) event.getEntity();
    int foodLevel = event.getFoodLevel();

    // 배고픔 최대치인지 확인
    if (foodLevel >= 20) {
        // 점수 주기
        plusScore(p, getLeftPlayTime());

        // 메세지, 소리
        sendMessages(ChatColor.GREEN + p.getName() + " 가 배고픔을 다 채웠습니다!");
        p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_FLUTE, 10.0F, 1.0F);

        // 플레이어 관전자모드로 변경
        p.setGameMode(GameMode.SPECTATOR);

        // 모든 사람이 끝난지 확인
        if (checkFinish()) {
            finishGame();
        }
    }
}

// 모든 플레이어의 배고픔이 다 채워진지 확인 
boolean checkFinish() {
    for (Player p : getPlayers()) {
        if (p.getGameMode() != GameMode.SPECTATOR) {
            return false;
        }
    }
    return true;
}

5. 기타

이제 대부분의 규칙들이 구현됬지만, 작업해줄 소소하지만 중요한 것들이 남았습니다

먼저 게임이 시작될 때 플레이어들이 낚시를 할 수 있게 낚시대를 지급해주고, 초기 배고픔 설정도 해줍니다

@Override
protected void onStart() {
    super.onStart();

    getPlayers().forEach(p -> {
        // 시작 배고픔 설정
        p.setFoodLevel(this.hunger);

        // 낚시대 주기
        ItemStack fishingRod = new ItemStack(Material.FISHING_ROD);
        ItemMeta meta = fishingRod.getItemMeta();
        meta.setUnbreakable(true);
        fishingRod.setItemMeta(meta);
        p.getInventory().addItem(fishingRod);
    });
}

마지막으로 튜토리얼도 등록해줍니다

@Override
protected List<String> tutorial() {
    return List.of("낚시해서 얻은 아이템으로 배고픔을 채우세요!", "낚시를 실패할 때마다 일정량의 배고픔이 줄어듭니다");
}

6. 전체 소스코드

public class HungryFishing extends SoloBattleMiniGame {

    private Map<Material, Integer> catchItems;
    private int hunger;
    private int failHunger;

    public HungryFishing() {
        super("HungryFishing", 2, 10, 60 * 5, 20);

        // 세팅
        getSetting().setIcon(Material.FISHING_ROD);

        // 옵션
        getCustomOption().set(Option.COLOR, ChatColor.AQUA);
        getCustomOption().set(Option.PLAYER_HURT, false);
        getCustomOption().set(Option.PVP, false);
        getCustomOption().set(Option.PVE, false);
    }

    @Override
    protected void initCustomData() {
        super.initCustomData();

        Map<String, Object> data = getCustomData();

        // 잡히는 아이템
        Map<String, Integer> itemList = new HashMap<>();
        itemList.put(Material.COOKIE.name(), 40);
        itemList.put(Material.MELON_SLICE.name(), 30);
        itemList.put(Material.CARROT.name(), 20);
        itemList.put(Material.COOKED_PORKCHOP.name(), 10);
        data.put("catch-items", itemList);

        // 배고픔
        data.put("hunger", 1);

        // 실패-배고픔
        data.put("fail-hunger", 2);
    }

    @Override
    public void loadCustomData() {
        super.loadCustomData();

        Map<String, Object> data = getCustomData();

        // 잡히는 아이템
        this.catchItems = new HashMap<>();
        Map<String, Integer> itemList = (Map<String, Integer>) data.get("catch-items");
        itemList.forEach((k, v) -> catchItems.put(Material.valueOf(k), v));

        // 배고픔
        this.hunger = (int) data.get("hunger");

        // 실패-배고픔
        this.failHunger = (int) data.get("fail-hunger");
    }

    @Override
    protected void initGame() {
    }

    @Override
    protected void onEvent(Event event) {
        if (event instanceof PlayerFishEvent) {
            onFishEvent((PlayerFishEvent) event);
        } else if (event instanceof FoodLevelChangeEvent) {
            onFoodLevelChange((FoodLevelChangeEvent) event);
        }
    }

    void onFishEvent(PlayerFishEvent event) {
        State state = event.getState();

        if (state == State.CAUGHT_FISH) {
            onCatch(event);
        } else if (state == State.FAILED_ATTEMPT) {
            onFail(event);
        }
    }

    void onCatch(PlayerFishEvent event) {
        Player p = event.getPlayer();

        Entity entity = event.getCaught();
        ItemStack item = ((Item) entity).getItemStack();

        // 잡힌 아이템을 랜덤 잡히는 아이템으로 설정
        item.setType(getRandomItem());

        // 메세지, 타이틀 보내기
        sendMessages(p.getName() + ChatColor.GREEN + "가 물고기 낚았습니다!");
        sendTitle(p, ChatColor.GREEN + "잡았다!", "");

        // 소리
        p.playSound(p.getLocation(), Sound.BLOCK_BELL_USE, 10.0F, 1.0F);
    }

    void onFail(PlayerFishEvent event) {
        Player p = event.getPlayer();

        // 플레이어 배고픔 실패-배고픔만큼 감소시키기
        int playerHunger = p.getFoodLevel() - this.failHunger;
        if (playerHunger < 1) {
            playerHunger = 1;
        }
        p.setFoodLevel(playerHunger);

        // 메세지, 타이틀 보내기
        sendMessages(p.getName() + ChatColor.RED + " 가 물고기를 낚는데 실패했습니다!");
        sendMessage(p, "당신은 " + this.failHunger + " 만큼 배고픔을 잃었습니다");
        sendTitle(p, ChatColor.RED + "물고기 도망쳤다!", "");

        // 소리
        p.playSound(p.getLocation(), Sound.BLOCK_ANVIL_DESTROY, 10.0F, 1.0F);
    }

    Material getRandomItem() {
        int random = new Random().nextInt(100);

        int range = 0;
        for (Entry<Material, Integer> entry : this.catchItems.entrySet()) {
            Material item = entry.getKey();
            int percent = entry.getValue();

            range += percent;

            if (random < range) {
                return item;
            }
        }

        // 안전
        return Material.COOKIE;
    }

    void onFoodLevelChange(FoodLevelChangeEvent event) {
        Player p = (Player) event.getEntity();
        int foodLevel = event.getFoodLevel();

        // 배고픔 최대치인지 확인
        if (foodLevel >= 20) {
            // 점수 주기
            plusScore(p, getLeftPlayTime());

            // 메세지, 소리
            sendMessages(ChatColor.GREEN + p.getName() + " 가 배고픔을 다 채웠습니다!");
            p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_FLUTE, 10.0F, 1.0F);

            // 플레이어 관전자모드로 변경
            p.setGameMode(GameMode.SPECTATOR);

            // 모든 사람이 끝난지 확인
            if (checkFinish()) {
                finishGame();
            }
        }
    }

    // 모든 플레이어의 배고픔이 다 채워진지 확인 
    boolean checkFinish() {
        for (Player p : getPlayers()) {
            if (p.getGameMode() != GameMode.SPECTATOR) {
                return false;
            }
        }
        return true;
    }

    @Override
    protected void onStart() {
        super.onStart();

        getPlayers().forEach(p -> {
            // 시작 배고픔 설정
            p.setFoodLevel(this.hunger);

            // 낚시대 주기
            ItemStack fishingRod = new ItemStack(Material.FISHING_ROD);
            ItemMeta meta = fishingRod.getItemMeta();
            meta.setUnbreakable(true);
            fishingRod.setItemMeta(meta);
            p.getInventory().addItem(fishingRod);
        });
    }

    @Override
    protected List<String> tutorial() {
        return List.of("낚시해서 얻은 아이템으로 배고픔을 채우세요!", "낚시를 실패할 때마다 일정량의 배고픔이 줄어듭니다");
    }

}

7. 플레이 비디오


제작 후

빌드 가이드 를 읽어서 미니게임을 테스트 해볼 수 있습니다


Warning
댓글이 없습니다.

새로운 댓글을 등록해 주세요!