게임에서 키 설정은 플레이어의 선호와 스타일에 맞춰 게임을 개인화하고, 플레이어의 편의성과 게임의 즐거움을 높이기 위해 필요합니다. 이를 위해 키 설정 시스템을 구현하고 이를 정리하였습니다.

 

✅ 구현

  • 키 설정 데이터를 관리하고 할당 여부를 검사하기위한 KeyManager와 키 설정을 하기위한 Ui슬롯 오브젝트를 관리하는 KeySettingController 두개의 클래스로 구현합니다.

 

· KeyManager

  • 키 설정 데이터를 관리하고 할당 여부를 검사하기위한 KeyManager 입니다.
더보기
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using Newtonsoft.Json;
using System.Text;

[System.Serializable]
public class KeyData
{
    //해당 키의 사용처(이름)
    public string keyName;

    //유니티에서 제공하는 KeyCode 값들
    //https://gist.github.com/Extremelyd1/4bcd495e21453ed9e1dffa27f6ba5f69
    public KeyCode keyCode; //json형태로 저장이 될 때는 KeyCode.I 가 아니라 106(숫자)로 저장이 된다. (enum)

    //KeyData 생성자
    public KeyData(string keyName, KeyCode keyCode)
    {
        this.keyName = keyName;
        this.keyCode = keyCode;
    }
}

/// <summary>
/// 키 입력에 대한 정보를 가지고있고, 특정한 기능에 대응하는 키를 관리하는 매니저 클래스
/// </summary>
public class KeyManager : Singleton<KeyManager>
{
    private static string mOptionDataFileName = "/KeyData.json"; //키 데이터 파일 이름
    private static string mFilePath;

    private Dictionary<string, KeyCode> mKeyDictionary;

    void Awake()
    {
        mKeyDictionary = new Dictionary<string, KeyCode>();
        mFilePath = Application.persistentDataPath + mOptionDataFileName;

        LoadOptionData();
    }

    private void LoadOptionData()
    {
        // 저장된 게임이 있다면
        if (File.Exists(mFilePath))
        {
            string fromJsonData = File.ReadAllText(mFilePath);

            List<KeyData> keyList = JsonConvert.DeserializeObject<List<KeyData>>(fromJsonData);

            foreach (var data in keyList)
            {
                mKeyDictionary.Add(data.keyName, data.keyCode);
            }
        }

        // 저장된 게임이 없다면
        else
        {
            Debug.Log(GetType() + " 파일이 없음");

            ResetOptionData();
        }
    }

    /// <summary>
    /// 프로젝트마다 별도로 해당 게임의 컨셉에 맞게 키를 설정한다.
    /// 스크립트에서 지정한 키로 재설정된다.
    /// </summary>
    private void ResetOptionData()
    {
        mKeyDictionary.Clear();

        //씬 내에서 사용할 키 데이터들//
        mKeyDictionary.Add("Inventory", KeyCode.I); //아이템 인벤토리
        mKeyDictionary.Add("Equipment", KeyCode.O); //장비 인벤토리
        mKeyDictionary.Add("Stat", KeyCode.P); //스탯
        mKeyDictionary.Add("Skill", KeyCode.K); //스킬
        mKeyDictionary.Add("Quest", KeyCode.Q); //퀘스트

        mKeyDictionary.Add("ItemQuickSlot0", KeyCode.Alpha1); //아이템 퀵슬롯 1번
        mKeyDictionary.Add("ItemQuickSlot1", KeyCode.Alpha2); //아이템 퀵슬롯 2번
        mKeyDictionary.Add("ItemQuickSlot2", KeyCode.Alpha3); //아이템 퀵슬롯 3번
        mKeyDictionary.Add("ItemQuickSlot3", KeyCode.Alpha4); //아이템 퀵슬롯 4번
        mKeyDictionary.Add("ItemQuickSlot4", KeyCode.Alpha5); //아이템 퀵슬롯 5번

        mKeyDictionary.Add("SkillQuickSlot0", KeyCode.Z); //스킬 퀵슬롯 1번
        mKeyDictionary.Add("SkillQuickSlot1", KeyCode.X); //스킬 퀵슬롯 2번
        mKeyDictionary.Add("SkillQuickSlot2", KeyCode.C); //스킬 퀵슬롯 3번
        mKeyDictionary.Add("SkillQuickSlot3", KeyCode.V); //스킬 퀵슬롯 4번
        mKeyDictionary.Add("SkillQuickSlot4", KeyCode.B); //스킬 퀵슬롯 5번  

        Debug.Log(GetType() + " 초기화");

        SaveOptionData();
    }

    public void SaveOptionData()
    {
        //딕셔너리에 있는 키 데이터들을 오브젝트 리스트를 이용하여 태그를 만들어서 직렬화시킨다.
        //리스트를 사용하지 않고 딕셔너리만 직렬화하면 태그가 없기에 사용할 수 없다. 오브젝트 형태(KeyData)로 만들고, Object type의 json 파일로 만들었다.
        //https://www.geeksforgeeks.org/json-data-types/#:~:text=JSON%20(JavaScript%20Object%20Notation)%20is,easy%20to%20understand%20and%20generate.

        //KeyData를 오브젝트로 담을 리스트
        List<KeyData> keys = new List<KeyData>();

        //모든 딕셔너리에 있는 키 값을 리스트에 넣어준다.
        foreach (KeyValuePair<string, KeyCode> keyName in mKeyDictionary)
        {
            keys.Add(new KeyData(keyName.Key, keyName.Value));
        }

        //List<KeyData>를 SeriaizeObject를 하면 Object type json이 나온다.
        string jsonData = JsonConvert.SerializeObject(keys);

        //파일로 쓰기
        FileStream fileStream = new FileStream(mFilePath, FileMode.Create);
        byte[] data = Encoding.UTF8.GetBytes(jsonData);
        fileStream.Write(data, 0, data.Length);
        fileStream.Close();

        Debug.Log(GetType() + " 파일 쓰기");
    }

    /// <summary>
    /// 키 이름을 기반으로 해당 키에 등록된 KeyCode를 리턴한다.
    /// </summary>
    /// <param name="keyName"></param>
    /// <returns></returns>
    public KeyCode GetKeyCode(string keyName)
    {
        return mKeyDictionary[keyName];
    }

    /// <summary>
    /// 해당 키에서 자기 자신을 제외한 키가 등록되어있는경우를 방지하고, 특정한 키 설정을 방지하기위해 키를 체크한다.
    /// </summary>
    /// <returns>할당 가능한 키인가?</returns>
    public bool CheckKey(KeyCode key, KeyCode currentKey)
    {
        //예외1. 현재 할당된 키에 같은 키로 설정하도록 한 경우는 허용으로 리턴한다.
        if(currentKey == key) { return true; }

        //1차 키 검사. 
        //키는 아래의 키만 허용한다.
        if
        (
            key >= KeyCode.A && key <= KeyCode.Z || //97 ~ 122   A~Z
            key >= KeyCode.Alpha0 && key <= KeyCode.Alpha9 || //48 ~ 57    알파 0~9
            key == KeyCode.Quote || //39         
            key == KeyCode.Comma || //44
            key == KeyCode.Period || //46
            key == KeyCode.Slash || //47
            key == KeyCode.Semicolon || //59
            key == KeyCode.LeftBracket || //91
            key == KeyCode.RightBracket || //93
            key == KeyCode.Minus || //45
            key == KeyCode.Equals || //61
            key == KeyCode.BackQuote //96
        ) { }
        else { return false; }

        //2차 키 검사. 
        //1차 키 검사를 포함한 키 중 다음 조건문 키는 설정할 수 없다.
        if
        (
            //이동 키 WASD
            key == KeyCode.W ||
            key == KeyCode.A ||
            key == KeyCode.S ||
            key == KeyCode.D
        ) { return false; }

        //3차 키 검사.
        //현재 설정된 키들 중 이미 할당된 키가 있는경우는 설정할 수 없다.
        foreach (KeyValuePair<string, KeyCode> keyPair in mKeyDictionary)
        {
            if (key == keyPair.Value)
            {
                return false;
            }
        }

        //모든 키 검사를 통과하면 해당 키는 설정이 가능한 키.
        return true;
    }

    /// <summary>
    /// keyName에 해당하는 키를 KeyCode인 key로 변경시킨다.
    /// </summary>
    /// <param name="keyCode">새로 설정하는 키의 코드값(enum)</param>
    /// <param name="keyName">설정된 키(keyCode)를 keyName에 할당한다</param>
    public void AssignKey(KeyCode keyCode, string keyName)
    {
        //딕셔너리 
        mKeyDictionary[keyName] = keyCode;

        //키 파일을 로컬에 저장
        SaveOptionData();
    }
}

 

[System.Serializable]
public class KeyData {}
  •  키 정보를 문자열 이름과 할당된 키를 매칭하는 데이터를 담습니다.

 

void Awake()
  • Scene이 처음으로 로딩되면 실행되며 키 매니저를 사용하기위한 준비를 합니다.
  • LoadOptionData()를 호출하여 옵션파일로부터 데이터를 읽어 설정을 합니다.

 

private void LoadOptionData()
  • 파일로부터 데이터를 읽어 설정을 합니다.
  • 파일이 없을경우 리턴되고 초기화를 진행합니다.

 

private void ResetOptionData()
  • 키 설정 옵션을 초기화합니다.
  • 이곳에서는 프로그램에서 사용하는 여러 기능들의 키를 지정합니다.

 

public void SaveOptionData()
  • 키 설정 옵션 데이터를 저장합니다.
  • Json 형태로 변환을 한 후에 로컬에 파일로 저장합니다.
  • 파일이 저장되는 형태는 다음과 같습니다.

 

public bool CheckKey(KeyCode key, KeyCode currentKey)
  • 가장 핵심이 되는 함수로 현재 키가 할당이 가능한지 확인합니다.
  • 다음 조건을 검사합니다.
  • 현재 같은 키로 설정을 시도하는가?
  • 허용하는 키 범위 이내인가?
  • 허용 키 범위 중 독립적으로 허용하지 않는 키 인가?
  • 다른 기능이 이미 이 키를 사용중인가?

 

 

public void AssignKey(KeyCode keyCode, string keyName)
  • 키를 할당하고 저장합니다.

 

· KeySettingController

  • 키 설정을 하기위한 Ui슬롯 오브젝트를 관리하는 KeySettingController 입니다.
더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using System;

public class KeySettingController : MonoBehaviour
{
    private KeyCode mOriginKeyCode;
    [SerializeField] private string mKeyBindingName;

    //키 설정 버튼
    [SerializeField] private Image mKeyButtonImage; //현재 할당된 키와 그 키를 수정할 수 있게 하는 버튼의 이미지
    private Coroutine mKeyButtonColorCor; //키 수정 버튼의 색상 변경을 수행하는 코루틴을 담는 변수

    //버튼 텍스트
    [SerializeField] private TextMeshProUGUI mKeyButtonText; //버튼의 하위 자식의 텍스트필드

    private void OnEnable()
    {
        mOriginKeyCode = KeyManager.Instance.GetKeyCode(mKeyBindingName);
        mKeyButtonText.text = ((char)mOriginKeyCode).ToString().ToUpper();
    }

    public void BTN_ModifyKey()
    {
        mKeyButtonText.text = "< >";

        StartCoroutine(CorAssignKey());
    }

    private IEnumerator CorAssignKey()
    {
        while (true)
        {
            if (Input.anyKeyDown)
            {
                foreach (KeyCode kcode in Enum.GetValues(typeof(KeyCode)))
                {
                    if (Input.GetKey(kcode))
                    {
                        // 기존의 코루틴 제거        
                        if (mKeyButtonColorCor != null) { StopCoroutine(mKeyButtonColorCor); }

                        // 키 설정을 할 수 있는경우?
                        if (KeyManager.Instance.CheckKey(kcode, mOriginKeyCode))
                        {
                            // 키 지정
                            KeyManager.Instance.AssignKey(kcode, mKeyBindingName);
                            mOriginKeyCode = kcode;

                            // 키 레이블을 변경
                            mKeyButtonText.text = ((char)kcode).ToString().ToUpper();

                            // 녹색으로 설정 완료됨을 연출
                            mKeyButtonColorCor = StartCoroutine(CorChangeButtonColor(Color.green));
                        }
                        else
                        {
                            // 키 레이블을 변경
                            mKeyButtonText.text = ((char)mOriginKeyCode).ToString().ToUpper();

                            // 빨간색으로 설정 완료됨을 연출
                            mKeyButtonColorCor = StartCoroutine(CorChangeButtonColor(Color.red));
                        }
                    }
                }

                yield break;
            }

            yield return null;
        }
    }

    private IEnumerator CorChangeButtonColor(Color targetColor, float colorSpeed = 2.0f)
    {
        float progress = 0;

        //targetColor로 변경
        while (true)
        {
            mKeyButtonImage.color = Color.Lerp(mKeyButtonImage.color, targetColor, progress);
            progress += colorSpeed * Time.deltaTime;

            //progress가 1이면 > 보간 완료
            if (progress > 1)
            {
                progress = 0;

                //targetColor에서 다시 돌아오기
                while (true)
                {
                    mKeyButtonImage.color = Color.Lerp(mKeyButtonImage.color, Color.white, progress);
                    progress += colorSpeed * Time.deltaTime;

                    //색상 전환 완료
                    if (progress > 1)
                    {
                        yield break;
                    }

                    yield return null;
                }
            }

            yield return null;
        }
    }
}

 

private void OnEnable()
{
    mOriginKeyCode = KeyManager.Instance.GetKeyCode(mKeyBindingName);
    mKeyButtonText.text = ((char)mOriginKeyCode).ToString().ToUpper();
}
  • 키설정 옵션이 활성화되는경우 현재 슬롯에 맞는 키를 가져오고 텍스트로 표시합니다.

 

public void BTN_ModifyKey()
{
    mKeyButtonText.text = "< >";

    StartCoroutine(CorAssignKey());
}
  • Ui로부터 호출되며 해당 슬롯에 키 설정을 시도합니다.

 

private IEnumerator CorAssignKey()
  • 코루틴을 이용하여 키 입력을 기다리고, 키를 입력받은경우 키 유효성 검사를합니다.
  • KeyManager.Instance.CheckKey(kcode, mOriginKeyCode)를 통해 현재 입력한 키가 설정 가능한 키인지 확인 후 설정이 가능하다면 해당 키로 슬롯에 지정된 키를 입력된 키로 변경합니다.

 

✅ UI 세팅

  • 하나의 슬롯이 하나의 키를 설정하도록 구현합니다.
  • 우측의 'I'라고 적혀있는 버튼은 키를 설정하기위한 버튼입니다.

 

  • 아래의 그림과 같이 하나의 슬롯에 Key Setting Controller 스크립트를 넣고, 설정을 해줍니다.

 

  • 버튼 Ui인 BTN_Assign Key 컴포넌트에 함수를 추가합니다.

 

  • 전체적인 디자인은 다음과같습니다.

 

✅ 사용 방법

private void TryOpenInventory()
{
    //옵션이 켜져있는경우 비활성화
    if (GameMenuManager.IsOptionActive) { return; }

    if (Input.GetKeyDown(KeyManager.Instance.GetKeyCode("Inventory")))
    {
        if (!IsInventoryActive)
            OpenInventory();
        else
            CloseInventory();
        ItemDescription.Instance.DisableToolTip();
    }
}
  • 인벤토리를 열기위해 키를 확인하는 코드입니다.
  • Input.GetKeyDown을 그대로 사용하되, 인자를 KeyManager.Instance.GeyKeyCode("문자열")을 이용하여 키 이름에 해당하는 값의 KeyCode를 리턴하여 해당 키가 눌렸는지 확인합니다.

 

✅ 예시

  • 처음에 인벤토리는 i키를 이용하여 열린것을 볼 수 있습니다.
  • 키 설정 슬롯에서 i를 8로 설정을 시도했으나, 8은 이미 다른곳에 할당되어있어 불가능하여 붉은색으로 나타난것을 볼 수 있습니다.
  • 후에 7로 설정을 완료한 후 i를 누르면 더이상 인벤토리창이 열리지 않고 7을 눌러야 창이 열리는것을 볼 수 있습니다.
bonnate