본문 바로가기
마인크래프트/플러그인 제작 강좌(자바)

[자바로 마크 Paper 플러그인 만들기]4. 유저 데이터 관리하기(3) - 다른 플레이어 조회하기

by Zepelown 2024. 6. 26.

*본 강의는 1.20.2 Paper 기준으로 제작되었습니다*

 

이전 강의

[자바로 마크 Paper 플러그인 만들기]3. 유저 데이터 관리하기(2) - 명령어 만들기 (tistory.com)

 

[자바로 마크 Paper 플러그인 만들기]3. 유저 데이터 관리하기(2) - 명령어 만들기

*본 강의는 1.20.2 Paper 기준으로 제작되었습니다* 이전 강의 [자바로 마크 Paper 플러그인 만들기]2. 유저 데이터 관리하기(1) - 이벤트 처리 (tistory.com) [자바로 마크 Paper 플러그인 만들기]2. 유저 데

zepelown.tistory.com

 

참고하면 좋은 강의

[인텔리제이로 마크 플러그인 개발하기]5. 나만의 명령어 만들기 (give, help 명령어 만들어보기) (tistory.com)

 

[인텔리제이로 마크 플러그인 개발하기]5. 나만의 명령어 만들기 (give, help 명령어 만들어보기)

*이 글은 Spigot 1.19.2 버전을 기준으로 하여 제작되었습니다. 이전화 https://zepelown.tistory.com/41 [인텔리제이로 마크 플러그인 개발하기]4. 나만의 아이템 제작하기 (ItemStack, ItemMeta에 관하여) *이 글은

zepelown.tistory.com

 

강의에 사용한 전체 코드

Zepelown/PaperBlogPosting: Minecraft Paper API Blog Posting Repository (github.com)

 

GitHub - Zepelown/PaperBlogPosting: Minecraft Paper API Blog Posting Repository

Minecraft Paper API Blog Posting Repository. Contribute to Zepelown/PaperBlogPosting development by creating an account on GitHub.

github.com

 

 


미리 보는 결과물 

그림 1. 다른 사람의 닉네임으로 검색할 수 있는 기능

'

/uinfo {유저닉네임}

{} 표시는 없어도 된다는 의미 -> /uinfo 입력 시 자신의 정보 표시

그림 2. 최종 파일 구조


개요

이전 강의에서 이어서 진행합니다.

 

기존에 작성된 코드를 개선함과

 

동시에 이전 강의에선 자신에 대한 정보만 볼 수 있었다면,

 

이젠 유저 닉네임을 이용하여 다른 유저의 정보를 볼 수 있게 만들어보겠습니다.

 


기능 구현 단계

 

먼저 유저 닉네임을 통해 검색을 한다는 말은 

 

유저 데이터의 정보를 비교해야 한다는 말이기도 합니다.

 

따라서 유저 데이터를 정의하고 있는 객체를 수정해줘야 합니다.

 

entity.User

public class User implements Comparable<User> {
    private UUID uuid;
    private String displayName;
    
    ...

    public User(String displayName){
        this.displayName = displayName;
    }

	...

    @Override
    public int compareTo(@NotNull User o) {
        return this.displayName.compareTo(o.displayName);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(displayName, user.displayName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(displayName);
    }
}

 

객체를 비교할 땐 Comparable 인터페이스를 사용하여 비교합니다.

 

compareTo, equlas, hashCode 메소드는 객체의 비교를 위한 기준을 제시합니다.

 

자세한 정보는 포털에 검색하시면 많은 정보가 나올겁니다.

 

그대로 복사해서 사용하시면 됩니다.

/ (참고) 전체 코드 /

더보기
public class User implements Comparable<User> {
    private UUID uuid;
    private String displayName;

    private String rank;

    private Long money;

    private String job;

    private String prefix;

    public User(String displayName){
        this.displayName = displayName;
    }

    public User(UUID uuid,   String displayName, String rank, Long money, String job, String prefix) {
        this.uuid = uuid;
        this.displayName = displayName;
        this.rank = rank;
        this.money = money;
        this.job = job;
        this.prefix = prefix;
    }

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getJob() {
        return job;
    }

    public void setJob(String job) {
        this.job = job;
    }

    public UUID getUuid() {
        return uuid;
    }

    public void setUuid(UUID uuid) {
        this.uuid = uuid;
    }

    public String getDisplayName() {
        return displayName;
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    public String getRank() {
        return rank;
    }

    public void setRank(String rank) {
        this.rank = rank;
    }

    public Long getMoney() {
        return money;
    }

    public void setMoney(Long money) {
        this.money = money;
    }

    @Override
    public int compareTo(@NotNull User o) {
        return this.displayName.compareTo(o.displayName);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(displayName, user.displayName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(displayName);
    }
}

 

common.util.HashMapUtil 과 service.UserManager

 

그림 3. 기존 코드(좌측)에서 바뀐 모습(우측)

 

기존엔 UserManager 객체에서

 

Player 객체를 비교하여 유저의 입퇴장 이벤트를 받아 유저의 데이터를 관리하였습니다.

 

이 방식에서 벗어나 유저 식별은 UUID를 통해 진행하도록 개선해 보겠습니다.

 

이렇게 변경을 하게 되면 한 가지 문제가 있습니다.

 

입력을 유저 닉네임, 즉 String으로 받는데 이에 대한 정보는 HashMap의 Value에 존재합니다.

 

Key 값으로 검색을 할 수 없게 됩니다.

 

따라서 따로 HashMap을 반복문을 이용하여 검색하는 메소드를 구현해야 합니다.

 

이를 common.util.HashMapUtil 이라는 final class를 만들어서 구현하겠습니다.

 

 

common.util.HashMapUtil 

그림 4. HashMapUtil 파일 구조 모습

 

import java.util.HashMap;
import java.util.Map;

public final class HashMapUtil {
    public static <K, V> K findKeyByValue(HashMap<K, V> map, V value) {
        for (Map.Entry<K, V> entry : map.entrySet()) {
            if (value.equals(entry.getValue())) {
                return entry.getKey();
            }
        }
        return null;
    }
}

 

위 코드는 제너릭이라는 기법을 사용하여 어떠한 HashMap이 오더라도 작동할 수 있도록 만든 메소드입니다.

 

value 검색하여 그에 대한 key를 반환하는 메소드입니다.

 

이것 또한 그냥 복사해서 사용하시면 됩니다.

 

이 메소드에 대한 이해는 참고 자료를 남기겠습니다.

 

/ (참고) 제네릭과 Map의 Entry에 관하여 /

더보기

service.UserManager

이제 유저 닉네임을 통해 유저의 UUID를 찾을 수 있게 되었습니다.

 

이를 이용하여 코드를 개선해 보겠습니다.

import java.util.HashMap;
import java.util.UUID;

import org.blog.paperedu.user.management.entity.User;
import org.bukkit.entity.Player;


import static org.blog.paperedu.common.HashMapUtil.findKeyByValue;

public class UserManager {
    private HashMap<UUID, User> onlineUserData = new HashMap<>();

    public void addUser(Player player) {
        UUID playerUUID = player.getUniqueId();
        User newUser = new User(
                playerUUID,
                player.getDisplayName(),
                "newbie",
                1000L,
                "jobless", "[뉴비]");
        onlineUserData.put(playerUUID, newUser);
    }

    public void removeUser(UUID playerUUID){
        onlineUserData.remove(playerUUID);
    }

    public User getUserData(UUID playerUUID){
        return onlineUserData.get(playerUUID);
    }

    public User getUserData(String playerName){
        return onlineUserData.get(findKeyByValue(onlineUserData, new User(playerName)));
    }

}

 

전체적으로 HashMap의 키가 UUID로 바뀜에 따라 코드의 수정이 있었습니다.

 

그림 5. 새로 만든 getUserData 메소드

 

추가로 getUserData를 오버로딩을 이용하여 만들어주었습니다.

 

두 번째 getUserData 메소드는 플레이어의 닉네임을 받으면 검색할 수 있게 해 줍니다.

 

여기서 new User(playerName)으로 새로 객체를 만드는 이유는

 

객체를 비교할만한 임시 객체를 만드는 겁니다.

 

이로써 앞서 Comparable 인터페이스의 메소드들(compareTo 등)을 사용할 수 있게 됩니다.

 

controller.UserConnectionController

public class UserConnectionController implements Listener {

    private final UserManager userManager;

    UserConnectionController(UserManager userManager){
        this.userManager = userManager;
    }

    @EventHandler
    public void onUserJoinServer(PlayerJoinEvent event) {
        userManager.addUser(event.getPlayer());
        PaperEdu.getServerInstance().getLogger().info("플레이어 데이터 저장");
    }

    @EventHandler
    public void onUserKickFromServer(PlayerKickEvent event) {
        userManager.removeUser(event.getPlayer().getUniqueId());
        PaperEdu.getServerInstance().getLogger().info("플레이어 데이터 삭제");
    }

    @EventHandler
    public void onUserQuitFromServer(PlayerQuitEvent event) {
        userManager.removeUser(event.getPlayer().getPlayerProfile().getId());
        PaperEdu.getServerInstance().getLogger().info("플레이어 데이터 삭제");
    }
}

 

UserManager가 UUID로 기준이 바뀌었기 때문에 

 

유저의 입퇴장 이벤트를 관리하는 UserConnectionController의 코드도 수정해 줍니다.

 


view.UserManagementView

그림 6. 이전 강의에서 언급된 View

 

 

이전 강의에서 MVC 패턴에서 View가 빠져있다고 잠깐 언급한 적이 있습니다.

 

명령어를 구성할 때 실질적으로 플레이어의 정보를 보여주는 기능을 가진 곳으로

 

이번에 코드를 개선하는 김에 만들어보겠습니다.

 

그림 7. view 패키지(폴더) 구조

 

import org.blog.paperedu.user.management.entity.User;
import org.bukkit.entity.Player;

public class UserManagementView {
    public void sendUserInfo(Player sender, User userData){
        sender.sendMessage("----------------------------");
        sender.sendMessage(String.format("이름 : %s", userData.getDisplayName()));
        sender.sendMessage(String.format("직업 : %s", userData.getJob()));
        sender.sendMessage(String.format("랭크 : %s", userData.getRank()));
        sender.sendMessage(String.format("칭호 : %s", userData.getPrefix()));
        sender.sendMessage(String.format("소지 금액 : %s", userData.getMoney()));
        sender.sendMessage("----------------------------");
    }
}

 

사실 별거 없습니다.

 

이전에 있던 코드를 분리시킨 것뿐이에요.

 

다만, 나중에 있을 사용자에게 정보를 표시할 때

 

여러 기능을 추가하려면 이 코드가 보다 이해하기 쉽습니다.

 

코드가 분리되었기 때문에 Controller들도 수정을 해야 하는데요 

 

이는 Controller의 주요 역할이 View와 Model 사이의 중재이기 때문입니다.

 

다시 말해,

 

Controller는 View와 관련된 객체와 Model과 관련된 객체를 모두 가지고 있어서 중재할 수 있는 거죠.

 

 

controller.UserManagementController

public class UserManagementController {

    private static UserManager userManager;

    private final UserManagementView userManagementView;

    private final PaperEdu serverInstance;

    private UserConnectionController userConnectionController;

    public UserManagementController() {
        this.userManager = new UserManager();
        this.userManagementView = new UserManagementView();
        this.serverInstance = PaperEdu.getServerInstance();

        this.userConnectionController = new UserConnectionController(userManager);

        registerCommands();
        registerEvents();
    }


    private void registerEvents() {
        serverInstance.getServer().getPluginManager().registerEvents(userConnectionController, serverInstance);
    }

    private void registerCommands() {
        serverInstance.getServer().getPluginCommand("uinfo").setExecutor(new UserInfoCommand(userManager, userManagementView));
    }
}

 

특별한 건 없습니다.

 

UserManagementView 객체를 UserManagementController가 가지고 있게 수정하였습니다.

 

controller.UserInfoCommand

이제 제일 중요한 명령어에 대한 로직을 수정해 보겠습니다.

 

package org.blog.paperedu.user.management.controller.commands;

import org.blog.paperedu.user.management.entity.User;
import org.blog.paperedu.user.management.service.UserManager;
import org.blog.paperedu.user.management.view.UserManagementView;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;

public class UserInfoCommand implements CommandExecutor {

    private final UserManager userManager;

    private final UserManagementView userManagementView;

    public UserInfoCommand(UserManager userManager, UserManagementView userManagementView){
        this.userManager = userManager;
        this.userManagementView = userManagementView;
    }

    @Override
    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
                             @NotNull String[] args) {
        User playerData;

        if (args.length == 1) {
            playerData = userManager.getUserData(args[0]);
            if (playerData == null) {
                sender.sendMessage("유저를 찾을 수 없습니다");
                return false;
            }
        } else {
            if (!(sender instanceof Player)) {
                sender.sendMessage("플레이어만 이 명령어를 사용할 수 있습니다.");
                return false;
            }
            Player player = (Player) sender;
            playerData = userManager.getUserData(player.getUniqueId());
        }

        userManagementView.sendUserInfo((Player) sender, playerData);

        playerData.setMoney(playerData.getMoney() + 10L);

        return true;
    }
}

 

그림 8. UserManagementView 객체를 생성자를 통해 받아오는 모습

 

먼저, 정보 표시에 필요한 UserManagementView 객체를 가져옵니다.

 

그림 8. 개선된 명령어 로직

 

지금 중요한 건 닉네임에 대한 정보를 어떻게 받는지예요.

 

이전 강의에서 언급된 args 배열에서 그 힌트를 찾을 수 있는데요.

 

args 배열은 명령어 파라미터의 값들을 가지고 있는 배열입니다.

 

/uinfo {유저닉네임} 에서 {유저닉네임} 부분을 가지고 있게 됩니다.

 

그림 9. args 배열의 크기를 검사하는 모습

 

파라미터의 개수는 많아질 수 있으므로 args의 크기를 먼저 검사합니다.

 

만약 크기가 1이라는 건 파라미터가 하나밖에 없다는 말이겠죠.

 

그림 10. args 배열의 크기가 0일 때 로직이 진행되는 모습

 

크기가 0이면? 기존에 하던 대로 명령어를 입력한 유저의 정보를 파악하면 됩니다.

 

또한, 유저닉네임의 파라미터가 존재하면 서버 콘솔에서도 입력이 가능해지므로 

 

크기가 0일 때만, 플레이어 검사를 해주면 되겠죠.

그림 11. 플레이어에게 정보를 표시하는 부분

 

이제 View로 가져온 UserManagementView의 sendUserInfo 메소드를 이용하여

 

결과만 표시해 주면 됩니다.

 

10원씩 돈을 추가하는 건 여전히 나둬도 작동하겠죠.

 

이제 결과를 확인해 볼까요?


 

결과

그림 12. 최종 결과

 

댓글