반응형

https://github.com/gloomn/blogMCPluginProjects

 

GitHub - gloomn/blogMCPluginProjects

Contribute to gloomn/blogMCPluginProjects development by creating an account on GitHub.

github.com

 

플러그인 코드들은 모두 여기 있으니 코드가 필요하시면 다운로드 하시면 됩니다!

이전 강의

https://syntaxack.tistory.com/entry/minecraftplugin8

 

마인크래프트 플러그인 강좌 8강 - 특정 아이템을 들면 버프 주기

이전강의저번 강좌에서는 쿨타임 시스템을 구현해 보았다.https://syntaxack.tistory.com/entry/minecraftplugin7 마인크래프트 플러그인 강좌 7강 - 쿨타임 시스템 구현하기이전 강의https://syntaxack.tistory.com/entr

syntaxack.tistory.com

 

GUI로 투표 시스템 만들기

1. 사용자가 /vote 명령어를 입력할 시

2. GUI 창이 열리고 투표할 목록이 생긴다.

3. 플레이어가 해당 아이템 슬롯을 누르면 투표가 된다.

4. 투표 중복을 방지하며, 투표가 완료되면 투표 결과가 출력된다.

 

오늘 만들어볼 것은 어떤 몬스터를 사냥할까? 라는 투표 주제로 투표를 해볼 것이다.

투표 후보자는 블레이즈, 좀비, 거미이다.

 

⚠️ 참고: 

마인크래프트 1.19+ 에서는 Adventure API가 새로 나왔기 때문에 ChatColor, getDisplayName()과 같은 함수들은 이제 deprecated(지원 안 함)되었다. 따라서 이 강의는 최신 문법을 적용하고 있다.

ChatColor -> Component.text("").color(NamedTextColor.RED); 와 같은 식이다.

 

프로젝트 만들기

Intellij를 실행해서 새로운 마인크래프트 프로젝트를 만들어준다.

프로젝트 생성 방법을 모르면 아래 링크를 눌러 한 번만 보면 된다.

https://syntaxack.tistory.com/entry/minecraftplugin1

 

마인크래프트 플러그인 강좌 1강 - 첫 플러그인 만들기

이전 강의https://syntaxack.tistory.com/entry/minecraftplugin0 마인크래프트 플러그인 강좌 0강 - 준비하기마인크래프트 플러그인이란?마인크래프트 플러그인은 마인크래프트 내에서 기능을 확장할 수 있도

syntaxack.tistory.com

 

 

1. paper/spigot/sponge 템플릿에서 여러분이 사용하는 버킷 플러그인을 선택한다.

2. 빌드 시스템은 Gradle를 선택한다.

3. 언어는 자바를 선택한다.

4. 마인크래프트 버전은 여러분이 플러그인을 적용항 버전과 paper 버전이 일치하도록 선택한다.

예를 들어서 마인크래프트 버전 1.21.4, paper 버전 1.21.4 이면 1.21.4를 선택한다.

5. 플러그인 이름과 클래스 이름을 적는다.

 

생성을 눌러준다.

https://syntaxack.tistory.com/entry/minecraftpluginproblem1

 

마인크래프트 플러그인 강좌 - 잘못된 Gradle JVM 구성을 발견했습니다.

IntelliJ를 사용해서 마인크래프트 플러그인 프로젝트를 만들었을 때 잘못된 Gradle JVM 설정이라는 알람이 뜰 때가 있다.이는 JDK와 Gradle이 호환되지 않아서 발생하는 문제이다.이때는 인터넷 검색

syntaxack.tistory.com

만약 Gradle과 JVM 버전이 안 맞는다는 오류가 뜨면 위의 포스트를 보면 된다.

 

Gradle Build 설정

우리는 jar 파일로 빌드할 때 빌드 위치를 바로 플러그인 폴더로 빌드되게 설정할 것이다.

build.gradle 파일을 열어준다.

tasks.jar{
    archiveFileName = 'voteSystem.jar'
    destinationDirectory = file('C:\\Users\\kijoon\\Desktop\\Server\\plugins')
}

jar 파일 이름과 대상 폴더를 설정해준다.

 

VoteSystem.java

package org.blog.voteSystem;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.CommandExecutor;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;

import java.util.*;

public class VoteSystem extends JavaPlugin implements Listener, CommandExecutor {

    private final Map<UUID, String> votes = new HashMap<>();
    private final List<String> options = Arrays.asList("블레이즈", "좀비", "거미");

    @Override
    public void onEnable() {
        Objects.requireNonNull(getCommand("vote")).setExecutor(this);
        Bukkit.getPluginManager().registerEvents(this, this);
        getLogger().info("VotePlugin 활성화됨!");
    }

    @Override
    public void onDisable() {
        getLogger().info("VotePlugin 비활성화됨!");
    }

    @Override
    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String @NotNull [] args) {
        if (!(sender instanceof Player player)) return true;
        openVoteGUI(player);
        return true;
    }

    public void openVoteGUI(Player player) {
        Inventory gui = Bukkit.createInventory(null, 9, Component.text("오늘 밤의 몬스터는?").color(NamedTextColor.DARK_GREEN));

        gui.setItem(2, createVoteItem(Material.BLAZE_ROD, "블레이즈"));
        gui.setItem(4, createVoteItem(Material.ROTTEN_FLESH, "좀비"));
        gui.setItem(6, createVoteItem(Material.STRING, "거미"));

        player.openInventory(gui);
    }

    private ItemStack createVoteItem(Material material, String name) {
        ItemStack item = new ItemStack(material);
        ItemMeta meta = item.getItemMeta();
        if (meta != null) {
            meta.displayName(Component.text(name).color(NamedTextColor.YELLOW));
            item.setItemMeta(meta);
        }
        return item;
    }

    @EventHandler
    public void onVoteClick(InventoryClickEvent event) {
        if (event.getView().title().equals(Component.text("오늘 밤의 몬스터는?").color(NamedTextColor.DARK_GREEN))) {
            event.setCancelled(true); // 아이템 이동 방지

            if (event.getCurrentItem() == null || event.getCurrentItem().getItemMeta() == null) return;

            Player player = (Player) event.getWhoClicked();

            String choice = PlainTextComponentSerializer.plainText().serialize(
                    Objects.requireNonNull(event.getCurrentItem().getItemMeta().displayName())
            );


            if (votes.containsKey(player.getUniqueId())) {
                player.sendMessage(Component.text("이미 투표했어요! 중복 투표는 안돼요!").color(NamedTextColor.RED));
                return;
            }

            votes.put(player.getUniqueId(), choice);
            player.closeInventory();
            player.sendMessage(
                    Component.text("투표 완료: ", NamedTextColor.DARK_GREEN)
                            .append(Component.text(choice, NamedTextColor.YELLOW))
            );

            broadcastVoteStatus();
        }
    }

    public void broadcastVoteStatus() {
        Map<String, Integer> resultCount = new HashMap<>();

        for (String option : options) {
            resultCount.put(option, 0);
        }

        for (String vote : votes.values()) {
            resultCount.put(vote, resultCount.getOrDefault(vote, 0) + 1);
        }

        Bukkit.broadcast(Component.text("\"\uD83D\uDCCA 현재 투표 현황:\"").color(NamedTextColor.AQUA));
        for (String option : options) {
            Bukkit.broadcast(Component.text(" - " + option + ": " + resultCount.get(option) + "표"));
        }
    }
}

코드 구성

이 코드는 먼저 사용자가 /vote를 입력하고, GUI 창이 나타났을 때 클릭 이벤트를 감지해야 하기 때문에

implements Listener, CommandExecutor을 하고 있다.

 

 

 

이 코드에는 7개의 함수가 존재한다.

onEnable()

onDisable()

onCommand()

openVoteGUI()

createVoteItem()

onVoteClick()

broadcastVoteStatus()

 

이렇게 존재한다.

onEnable()과  onDisable()은 아래 강의를 참조바란다.

https://syntaxack.tistory.com/entry/minecraftplugin1

 

마인크래프트 플러그인 강좌 1강 - 첫 플러그인 만들기

이전 강의https://syntaxack.tistory.com/entry/minecraftplugin0 마인크래프트 플러그인 강좌 0강 - 준비하기마인크래프트 플러그인이란?마인크래프트 플러그인은 마인크래프트 내에서 기능을 확장할 수 있도

syntaxack.tistory.com

변수 등록

private final Map<UUID, String> votes = new HashMap<>();

플레이어의 UUID를 키로, 투표 선택지를 값을 저장하는 맵이다.

<1번 플레이어의 UUID, 좀비>

이런식으로 엮인다.

 

private final List<String> options = Arrays.asList("블레이즈", "좀비", "거미");

가능한 투표 항목 목록을 리스트로 보관한다.

 

onCommand 함수

@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String @NotNull [] args) {
    if (!(sender instanceof Player player)) return true;
    openVoteGUI(player);
    return true;
}

/vote 명령어를 입력한 사람(sender)이 플레이어일 경우 GUI를 열어준다.

명령어 입력자가 콘솔이면 아무 일도 안 하는 코드이다.

위의 내용을 자세히 알고 싶으면 아래 강의를 보기를 바란다.

https://syntaxack.tistory.com/entry/minecraftplugin2

 

마인크래프트 플러그인 강좌 2강 - 커스텀 커맨드 구현

이전 강의https://syntaxack.tistory.com/entry/minecraftplugin1 마인크래프트 플러그인 강좌 1강 - 첫 플러그인 만들기이전 강의https://syntaxack.tistory.com/entry/minecraftplugin0 마인크래프트 플러그인 강좌 0강 - 준비

syntaxack.tistory.com

 

openVoteGUI 함수

public void openVoteGUI(Player player) {

이 함수는 인수로 player을 받는다.

따라서 위에 onCommand 부분에서 openVoteGUI(player)로 함수를 사용한다.

Inventory gui = Bukkit.createInventory(null, 9, Component.text("오늘 밤의 몬스터는?").color(NamedTextColor.DARK_GREEN));

이 코드는 9칸짜리 인벤토리를 만들고, 제목은 초록색으로 "오늘 밤의 몬스터는"이라고 설정했다.

gui.setItem(2, createVoteItem(Material.BLAZE_ROD, "블레이즈"));
gui.setItem(4, createVoteItem(Material.ROTTEN_FLESH, "좀비"));
gui.setItem(6, createVoteItem(Material.STRING, "거미"));

인벤토리 2,4,6 번 칸에 아이템을 넣는다.

각 아이템은 각각의 몬스터를 나타낸다.

player.openInventory(gui);}

해당 GUI를 플레이어게 열어준다.

 

createVoteItem 함수

private ItemStack createVoteItem(Material material, String name) {
    ItemStack item = new ItemStack(material);

지정한 아이템으로 ItemStack을 생성한다.

위의 openVoteGUI 함수에서 createVoteItem(Material.BLAZE_ROD, "블레이즈")로 사용된다.

이따 블레이즈 막대기 ItemStack을 생성하는 것이다.

 

    ItemMeta meta = item.getItemMeta();
    if (meta != null) {
        meta.displayName(Component.text(name).color(NamedTextColor.YELLOW));
        item.setItemMeta(meta);
    }

아이템 이름을 노란색으로 지정하여 보기 좋게 설정한다.

그 다음 아이템 메타를 적용한다. 메타는 쉽게 말해 아이템에 대한 설명이라고 할 수 있다.

    return item;
}

GUI에 배치할 준비된 아이템을 반환한다.

 

즉 이 함수의 인자는 아이템과 문자열을 받는다. 이때 아이템의 이름을 받은 문자열로 설정하고, 노란색으로 입힌다. 그다음 아이템을 반환한다.

따라서 createVoteItem(Material.BLAZE_ROD, "블레이즈")는

블레이즈 막대기의 이름을 노란색으로 된 "블레이즈"로 만든 다음에 openVoteGUI로 return, 보내는 것이다.

 

onVoteClick 함수

@EventHandler
public void onVoteClick(InventoryClickEvent event) {
    if (event.getView().title().equals(Component.text("오늘 밤의 몬스터는?").color(NamedTextColor.DARK_GREEN))) {

플레이어가 열어둔 GUI의 제목이 투표창인지 확인한다.

 

        event.setCancelled(true); // 아이템 이동 방지

GUI 안의 아이템을 클릭해도 움직이지 않도록 막는다.

 

        if (event.getCurrentItem() == null || event.getCurrentItem().getItemMeta() == null) return;

클릭된 아이템이 없거나 메타데이터가 없으면 종료한다.

 

        Player player = (Player) event.getWhoClicked();

GUI를 클릭한 사람을 Player로 캐스팅(정한다)한다.

 

        String choice = PlainTextComponentSerializer.plainText().serialize(
                Objects.requireNonNull(event.getCurrentItem().getItemMeta().displayName())
        );

아이템의 이름을 가져와 색상 없이 텍스트로 변환해 choice 변수에 저장한다.

즉 플레이어가 선택한 아이템을 choice에 텍스트로 저장하는 것이다.

PlainTextComponentSerializer.plainText().serialize(...) 는 Adventure의 Component를 순수 문자열로 변환한다.

Objects.requireNonNull(event.getCurrentItem().getItemMeta().displayName()) 는 클릭 이벤트에서 감지한 현재 아이템의 메타데이터에서 이름을 가져오는데 NULL이면 안된다.

 

        if (votes.containsKey(player.getUniqueId())) {
            player.sendMessage(Component.text("이미 투표했어요! 중복 투표는 안돼요!").color(NamedTextColor.RED));
            return;
        }

해당 플레이어가이미 투표했는지 확인하고, 했으면 경고 메시지 출력 후 종료한다. 이때 player.getUniqueId를 사용하기 때문에 게임을 나갔다 들어와도 중복 투표가 안 된다.

 

        votes.put(player.getUniqueId(), choice);

해당 플레이어의 투표 선택지를 저장한다.

아까 변수 선언 부분의 votes는 맵으로<UUID, choice>로 되어 있다.

 

        player.closeInventory();

투표 GUI 창을 닫는다.

 

        player.sendMessage(
                Component.text("투표 완료: ", NamedTextColor.DARK_GREEN)
                        .append(Component.text(choice, NamedTextColor.YELLOW))
        );

플레이어가 투표한 선택지를 플레이어게 채팅창으로 알려준다.

 

        broadcastVoteStatus();
    }
}

모든 플레이어게 투표 현황을 알린다.

 

이제 broadcastVoteStatus() 함수를 살펴보자.

 

broadcastVoteStatus 함수

public void broadcastVoteStatus() {
    Map<String, Integer> resultCount = new HashMap<>();

선택지별 투표 수를 집계할 새 맵을 생성한다.

투표 수를 집계 해야 하니까 Map<String, Integer>로 선언한다.

 

    for (String option : options) {
        resultCount.put(option, 0);
    }

"블레이즈", "좀비", "거미" 초기 표 수를 0으로 설정한다.

 

    for (String vote : votes.values()) {
        resultCount.put(vote, resultCount.getOrDefault(vote, 0) + 1);
    }

저장된 투표 결과를 기반으로 개수를 누적한다.

 

    Bukkit.broadcast(Component.text("\"\uD83D\uDCCA 현재 투표 현황:\"").color(NamedTextColor.AQUA));

전체 플레이어에게 제목 메시지(📊 포함)를 보냄

 

    for (String option : options) {
        Bukkit.broadcast(Component.text(" - " + option + ": " + resultCount.get(option) + "표"));
    }
}

각 항목별로 표 수를 출력한다.

 

⚠️ 참고: Bukkit.broadcast(Component)는 Spigot에서 지원되지 않음

이 강의 paper 플러그인을 기반으로 하고 있기 때문에 broadcast(Component)는 Spigot에서 지원하지 않는다.

 

plugin.yml 작성하기

name: voteSystem
version: '1.0-SNAPSHOT'
main: org.blog.voteSystem.VoteSystem
api-version: '1.21'
commands:
  vote:
    description: 몬스터 투표 GUI 열기

 

테스트하기

gradle build에서 jar을 선택하고 빌드한다.

그 다음 서버를 실행해서 플러그인을 테스트해본다.

/vote를 입력하면 위와 같이 GUI 창이 열린다.

 

좀비를 클릭해보겠다.

이렇게 정상적으로 작동하는 것을 볼 수 있다.

 

한 번 중복 투표 방지 시스템도 작동하는지 보자.

잘 작동한다.

 

궁금한 점이나 코드 질문은 댓글로 ㄱㄱ

반응형