using TMPro;
using UnityEngine;
public class Cash : MonoBehaviour
{
public TextMeshProUGUI cashText;
public int cash;
void Start()
{
cashText.text = cash.ToString("N0");
cashText.text = string.Format("{0:N0}", cash);
}
}
- 현금의 천 원 단위마다 콤마(,)을 찍어주는 두 가지 방법
STEP 2. 유저 데이터 제작
public class UserData : MonoBehaviour
{
public string userName;
public int cash;
public int balance;
}
public class UserInfo : MonoBehaviour
{
public TextMeshProUGUI nameText;
public TextMeshProUGUI cashText;
public TextMeshProUGUI balanceText;
public UserData userData;
void Start()
{
nameText.text = userData.userName;
cashText.text = userData.cash.ToString("N0");
balanceText.text = userData.balance.ToString("N0");
}
}
- 유저 정보에다 받아온 유저 데이터를 넣어주는 코드
STEP 3. 게임 매니저 제작
[System.Serializable]
public class UserData
{
public string name;
public int cash;
public int balance;
public UserData(string name, int cash, int balance)
{
this.name = name;
this.cash = cash;
this.balance = balance;
}
}
public class GameManager : MonoBehaviour
{
public static GameManager Instance;
public UserData userData;
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
userData = new UserData("Gee", 100000, 50000);
}
}
public class UserInfo : MonoBehaviour
{
public TextMeshProUGUI nameText;
public TextMeshProUGUI cashText;
public TextMeshProUGUI balanceText;
void Start()
{
UserData userData = GameManager.Instance.userData;
nameText.text = userData.name;
cashText.text = userData.cash.ToString("N0");
balanceText.text = userData.balance.ToString("N0");
}
}
- GameManager를 싱글톤화 한 후 UserData 클래스의 매개변수를 가져와 객체를 생성(초기화)
- 이후 UserInfo를 통해 각 텍스트에 맞는 정보를 넣어준다. (출력하는 역할)
- GameManager가 싱글톤으로 초기화되고, UserData 객체를 생성해 유저 데이터를 설정 -> UserInfo가 GameManager에서 UserData를 참조하여 UI에 데이터를 표시
- GameManager가 게임 전반의 상태를 관리하며 UserData를 중앙에서 보유함
- 다른 스크립트에서도 GameManager.Instance.userData로 접근 가능
- UserInfo는 UI에 데이터를 표시하는 역할만 맡고 데이터는 직접 관리하지 않음
- UserData 클래스는 Unity에서 인스펙터로 사용될 일이 없기 때문에 MonoBehaviour를 제거하고 순수 데이터 클래스로 변경 (데이터를 저장만 함)
STEP 4. 데이터와 UI 연동
[System.Serializable]
public class UserData
{
public string name;
public int cash;
public int balance;
public UserData(string name, int cash, int balance)
{
this.name = name;
this.cash = cash;
this.balance = balance;
}
}
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
[field: SerializeField] public UserData userData { get; private set; }
public UserInfo userInfo;
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
return;
}
userData = new UserData("Gee", 100000, 50000);
}
public void Start()
{
if (userInfo == null)
{
userInfo = FindObjectOfType<UserInfo>();
}
UpdateName("GEEEEE");
UpdateCash(500);
UpdateBalance(500);
}
public void UpdateName(string newName)
{
userData.name = newName;
userInfo.Refresh();
}
public void UpdateCash(int amount)
{
userData.cash += amount;
userInfo.Refresh();
}
public void UpdateBalance(int amount)
{
userData.balance += amount;
userInfo.Refresh();
}
}
public class UserInfo : MonoBehaviour
{
public TextMeshProUGUI nameText;
public TextMeshProUGUI cashText;
public TextMeshProUGUI balanceText;
void Start()
{
Refresh();
}
public void Refresh()
{
UserData userData = GameManager.Instance.userData;
nameText.text = userData.name;
cashText.text = userData.cash.ToString("N0");
balanceText.text = userData.balance.ToString("N0");
}
}
- GameManager와 UserData에
{ get; private set; }을 추가해 가져올 수만 있고 수정은 불가능하게 설정
- 유니티 인스펙터에서 유저 데이터를 확인하기 위해
[field: SerializeField] 사용. 이러면 수정은 가능하나, 다른 UserData로 바꾸는 건 불가능하다.
- 이는 { get; private set; }이 setter를 private으로 제한하기 때문
- 이후 UserInfo의 Start()안에 있던 코드들을 Refresh()로 옮겨주고, GameManager에서 userInfo를 찾아 새로운 데이터를 업데이트 해 줄 수 있도록 설정.
- 이렇게 하면 플레이 시 변경된 값인
GEEEEE, 100,500, 50,500이 출력된다.
- GameManager Awake에서 Destroy한 다음에는 return을 해줘야 불필요한 코드 실행을 방지할 수 있다. (userData의 불필요한 초기화를 막음)
- return은 메서드의 실행을 즉시 종료시켜서 Awake 메서드의 나머지 부분(예: userData = new UserData ~)이 실행되지 않도록 막아준다.
- Destroy를 해줬다고 해도 Unity의 프레임 기반 업데이트 방식 때문에 Awake 메서드가 끝날 때까지는 즉시 사라지지 않는다는 점을 기억하자.
- Awake 메서드가 끝날 때까지 객체가 메모리에 남아 있음 -> new UserData의 불필요한 초기화
STEP 5. 입금 UI 제작


- Vertical Layout Group을 사용해 버튼마다 동일한 간격으로 배치
STEP 6. 출금 UI 제작
STEP 7. 입출금 창 이동
public class PopupBank : MonoBehaviour
{
public GameObject depositPanel;
public GameObject withdrawPanel;
public GameObject buttonPanel;
public void OpenDepositPanel()
{
buttonPanel.SetActive(false);
depositPanel.SetActive(true);
}
public void OpenWithdrawPanel()
{
buttonPanel.SetActive(false);
withdrawPanel.SetActive(true);
}
public void ClosePanel()
{
depositPanel.SetActive(false);
withdrawPanel.SetActive(false);
buttonPanel.SetActive(true);
}
}
- 해당 스크립트를 PopupBank 오브젝트에 넣고, 각 버튼들의 OnClick에 메서드를 넣어 입출금 창이 활성화/비활성화 되도록 설정
STEP 8. 입금 기능 만들기
public void DepositAmount(int amount)
{
if (GameManager.Instance.userData.cash >= amount)
{
GameManager.Instance.userData.cash -= amount;
GameManager.Instance.UpdateBalance(amount);
}
else
{
errorPanel.SetActive(true);
}
}
public void DepositInputAmount()
{
if(int.TryParse(depositInput.text, out int amount))
{
DepositAmount(amount);
}
else
{
depositInput.text = "";
}
depositInput.text = "";
}

- 각 금액 버튼들에 매개변수 amount를 지정해 주고, 해당 금액만큼의 cash를 보유하고 있다면
cash -= amount
- 잔액도 amount 값만큼 추가하여 Update
- depositInput에서 직접 입력한 값은 해당 금액만큼 amount에 추가하여 동일하게 Update 진행
- 입금하려는 금액이 cash보다 크다면 errorPanel 활성화
- 입력한 텍스트가 숫자가 아니라면
depositInput.text = "";으로 텍스트 창 초기화
STEP 9. 출금 기능 만들기
public void WithdrawAmount(int amount)
{
if (GameManager.Instance.userData.balance >= amount)
{
GameManager.Instance.userData.balance -= amount;
GameManager.Instance.UpdateCash(amount);
}
else
{
errorPanel.SetActive(true);
}
}
public void WithdrawInputAmount()
{
if (int.TryParse(withdrawInput.text, out int amount))
{
WithdrawAmount(amount);
}
else
{
withdrawInput.text = "";
}
withdrawInput.text = "";
}
STEP 10. 저장 및 로드 기능 만들기
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
[field: SerializeField] public UserData userData { get; private set; }
public UserInfo userInfo;
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
return;
}
LoadUserData();
}
public void Start()
{
if (userInfo == null)
{
userInfo = FindObjectOfType<UserInfo>();
}
SaveUserData();
}
public void UpdateName(string newName)
{
userData.name = newName;
userInfo.Refresh();
SaveUserData();
}
public void UpdateCash(int amount)
{
userData.cash += amount;
userInfo.Refresh();
SaveUserData();
}
public void UpdateBalance(int amount)
{
userData.balance += amount;
userInfo.Refresh();
SaveUserData();
}
public void SaveUserData()
{
PlayerPrefs.SetString("Name", userData.name);
PlayerPrefs.SetInt("Cash", userData.cash);
PlayerPrefs.SetInt("Balance", userData.balance);
PlayerPrefs.Save();
}
public void LoadUserData()
{
if (PlayerPrefs.HasKey("Name"))
{
string name = PlayerPrefs.GetString("Name");
int cash = PlayerPrefs.GetInt("Cash");
int balance = PlayerPrefs.GetInt("Balance");
userData = new UserData(name, cash, balance);
}
else
{
userData = new UserData("Gee", 100000, 50000);
}
}
}
- PlayerPrefs를 활용한 userData 저장
- Awake에서 new UserData로 생성하던 객체를 LoadUserData();로 변경
PlayerPrefs.SetString, PlayerPrefs.SetInt로 Name, Cash, Balance 저장
if (PlayerPrefs.HasKey("Name"))로 저장한 데이터를 찾고 string, int 변수에 담아 로드
- 저장된 데이터가 없다면 new UserData -> 기본값 "Gee", 100000, 50000으로 객체 생성
STEP 11. 로그인 창 만들기

public UserInfo userInfo;
public GameObject popupBank;
public GameObject popupLogin;
public const string nameKey = "Name";
public const string cashKey = "Cash";
public const string balanceKey = "Balance";
public const string idKey = "Id";
public const string passwordKey = "Password";
public void Start()
{
if (userInfo == null)
{
userInfo = FindObjectOfType<UserInfo>();
}
SaveUserData();
popupBank.SetActive(false);
popupLogin.SetActive(true);
}
public void SaveUserData()
{
PlayerPrefs.SetString(nameKey, userData.name);
PlayerPrefs.SetInt(cashKey, userData.cash);
PlayerPrefs.SetInt(balanceKey, userData.balance);
PlayerPrefs.SetString(idKey, userData.id);
PlayerPrefs.SetString(passwordKey, userData.password);
PlayerPrefs.Save();
}
public void LoadUserData()
{
if (PlayerPrefs.HasKey(nameKey))
{
string name = PlayerPrefs.GetString(nameKey);
int cash = PlayerPrefs.GetInt(cashKey);
int balance = PlayerPrefs.GetInt(balanceKey);
string id = PlayerPrefs.GetString(idKey);
string password = PlayerPrefs.GetString(passwordKey);
userData = new UserData(name, cash, balance, id, password);
}
else
{
userData = new UserData("Gee", 100000, 50000, "", "");
}
}
}
- PlayerPrefs에 id와 password를 추가하고,
public const string nameKey = "Name";과 같이 저장 값을 상수로 지정
- 오타를 방지하고 유지보수를 편하게 하기 위함 (+가독성 향상, 메모리 성능 최적화)
- 게임 시작 시 popupBank 창은 꺼주고 popupLogin 창을 활성화 시켜줌
- PSInputField 오브젝트는 Content Type을 Password로 변경해 텍스트 입력 시 ***로 표시되게 설정
STEP 12. 회원가입 만들기

public class PopupSignUp : MonoBehaviour
{
public TMP_InputField inputId;
public TMP_InputField inputName;
public TMP_InputField inputPassword;
public TMP_InputField inputPasswordConfirm;
public GameObject popupSignUp;
public GameObject errorPanel;
public TextMeshProUGUI errorText;
public void SignUp()
{
if (string.IsNullOrWhiteSpace(inputId.text))
{
errorText.text = "ID를 확인해주세요.";
OpenErrorPanel();
return;
}
if (string.IsNullOrWhiteSpace(inputName.text))
{
errorText.text = "Name을 확인해주세요.";
OpenErrorPanel();
return;
}
if (string.IsNullOrWhiteSpace(inputPassword.text))
{
errorText.text = "Password를 확인해주세요.";
OpenErrorPanel();
return;
}
if (string.IsNullOrEmpty(inputPasswordConfirm.text))
{
errorText.text = "Password Confirm을 확인해주세요.";
OpenErrorPanel();
return;
}
if (inputPassword.text != inputPasswordConfirm.text)
{
errorText.text = "Password가 같지 않습니다.";
OpenErrorPanel();
return;
}
if (GameManager.Instance != null)
{
GameManager.Instance.SetUserData(new UserData(name: inputName.text, cash: 100000, balance: 50000,
id: inputId.text, password: inputPassword.text));
}
ClosePopupSignUp();
}
public void OpenPopupSignUp()
{
popupSignUp.SetActive(true);
}
public void ClosePopupSignUp()
{
popupSignUp.SetActive(false);
}
public void OpenErrorPanel()
{
errorPanel.SetActive(true);
}
public void CloseErrorPanel()
{
errorPanel.SetActive(false);
}
}
- 입력 정보들이 비어있는지 확인 후 순차적으로 오류 메세지 출력(ErrorPanel은 항상 활성화)
- 모든 내용을 맞게 입력했고, GameManager가 있다면 SetUserData에 new UserData 객체를 넣어줌(입력한 정보 + 기본 cash, balance값)
using System.IO;
using UnityEngine;
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
[field: SerializeField] public UserData userData { get; private set; }
public UserInfo userInfo;
public GameObject popupBank;
public GameObject popupLogin;
private string savePath;
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
return;
}
savePath = Path.Combine(Application.persistentDataPath, "userData.json");
Debug.Log($"Json 파일 경로: {savePath}");
LoadUserData();
}
public void SetUserData(UserData newUserData)
{
userData = newUserData;
SaveUserData();
if (userInfo != null)
{
userInfo.Refresh();
}
}
public void SaveUserData()
{
string json = JsonUtility.ToJson(userData);
File.WriteAllText(savePath, json);
Debug.Log($"저장 성공: {json}");
}
public void LoadUserData()
{
if (File.Exists(savePath))
{
string json = File.ReadAllText(savePath);
userData = JsonUtility.FromJson<UserData>(json);
Debug.Log($"불러온 데이터: {json}");
}
else
{
Debug.Log("저장된 데이터가 없습니다.");
}
}
}
- 데이터 저장 방식을 PlayerPrefs에서 JSON으로 변경
- Awake에서 저장 경로를 설정 -> (받아온 newUserData를 SetUserData에서 저장) -> newUserData를 JSON 경로에 저장(직렬화)
- 데이터를 로드한 땐 JSON 파일을 찾아 문자열을 읽은 후 userData에 저장(역직렬화)
STEP 13. 로그인하기
public UserData LoadUserData()
{
if (File.Exists(savePath))
{
string json = File.ReadAllText(savePath);
userData = JsonUtility.FromJson<UserData>(json);
Debug.Log($"불러온 데이터: {json}");
return userData;
}
else
{
Debug.Log("저장된 데이터가 없습니다.");
return null;
}
}
- 기존의
public void LoadUserData()를 public UserData LoadUserData()로 변경. 반환값을 바꿔줌
public class PopupLogin : MonoBehaviour
{
public TMP_InputField inputID;
public TMP_InputField inputPS;
public GameObject PopupLoginPanel;
public GameObject PopupBank;
public GameObject errorPanel;
public TextMeshProUGUI errorText;
private string savePath;
private void Awake()
{
savePath = Path.Combine(Application.persistentDataPath, "userData.json");
Debug.Log($"파일 경로: {savePath}");
}
public void Login()
{
UserData loadUserData = GameManager.Instance.LoadUserData();
if (string.IsNullOrWhiteSpace(inputID.text))
{
errorText.text = "ID를 입력해주세요.";
OpenErrorPanel();
return;
}
if (string.IsNullOrWhiteSpace(inputPS.text))
{
errorText.text = "Password를 입력해주세요.";
OpenErrorPanel();
return;
}
if (loadUserData == null)
{
errorText.text = "해당 데이터가 없습니다.";
OpenErrorPanel();
inputID.text = "";
inputPS.text = "";
return;
}
if (inputID.text == loadUserData.id && inputPS.text == loadUserData.password)
{
PopupLoginPanel.SetActive(false);
PopupBank.SetActive(true);
Debug.Log("로그인 성공");
}
else
{
errorText.text = "ID 또는 Password가 일치하지 않습니다.";
OpenErrorPanel();
inputID.text = "";
inputPS.text = "";
}
}
public void OpenErrorPanel()
{
errorPanel.SetActive(true);
}
public void CloseErrorPanel()
{
errorPanel.SetActive(false);
}
}
Login()에서 UserData loadUserData = GameManager.Instance.LoadUserData();를 통해 GameManager에 저장된 UserData를 접근해 로그인 체크
- JSON에 저장되어 있는 정보와 일치하면 로그인 창을 닫고 PopupBank 창을 활성화 시켜줌
STEP 14. 송금하기

[System.Serializable]
public class UserData
{
public string name;
public int cash;
public int balance;
public string id;
public string password;
public UserData(string name, int cash, int balance, string id, string password)
{
this.name = name;
this.cash = cash;
this.balance = balance;
this.id = id;
this.password = password;
}
}
[System.Serializable]
public class UserDataList
{
public List<UserData> users = new List<UserData>();
}
- UserData를 리스트화 시켜서 여러 데이터를 저장할 수 있게 함
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
[field: SerializeField] public UserData userData { get; private set; }
private UserDataList userDataList = new UserDataList();
private UserData loginUser;
public UserInfo userInfo;
public GameObject popupBank;
public GameObject popupLogin;
private string savePath;
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
return;
}
savePath = Path.Combine(Application.persistentDataPath, "users.json");
Debug.Log($"Json 파일 경로: {savePath}");
LoadUserData();
}
public void Start()
{
if (userInfo == null)
{
userInfo = FindObjectOfType<UserInfo>();
}
SaveUserData();
popupBank.SetActive(false);
popupLogin.SetActive(true);
}
public UserData GetCurrentUser()
{
return loginUser;
}
public void SetUserData(UserData newUserData)
{
UserData existingUser = null;
foreach (UserData user in userDataList.users)
{
if (user.id == newUserData.id)
{
existingUser = user;
break;
}
}
if (existingUser != null)
{
existingUser.name = newUserData.name;
existingUser.cash = newUserData.cash;
existingUser.balance = newUserData.balance;
existingUser.password = newUserData.password;
}
else
{
userDataList.users.Add(newUserData);
}
userData = newUserData;
loginUser = newUserData;
userInfo?.Refresh();
SaveUserData();
}
public void SaveUserData()
{
string json = JsonUtility.ToJson(userDataList, true);
File.WriteAllText(savePath, json);
Debug.Log($"유저 데이터리스트 저장 성공: {json}");
}
public void LoadUserData()
{
if (File.Exists(savePath))
{
string json = File.ReadAllText(savePath);
Debug.Log($"불러온 데이터: {json}");
userDataList = JsonUtility.FromJson<UserDataList>(json);
Debug.Log($"불러온 유저 수: {userDataList.users.Count}");
}
else
{
Debug.Log("저장된 데이터가 없습니다.");
userDataList = new UserDataList();
}
}
public UserData FindUserID(string userID)
{
foreach (UserData user in userDataList.users)
{
if (user.id == userID)
{
return user;
}
}
return null;
}
public void UpdateName(string newName)
{
userData.name = newName;
userInfo.Refresh();
SaveUserData();
}
public void UpdateCash(int amount)
{
userData.cash += amount;
userInfo.Refresh();
SaveUserData();
}
public void UpdateBalance(int amount)
{
userData.balance += amount;
userInfo.Refresh();
SaveUserData();
}
}
FindUserID: userDataList.users에서 ID가 일치하는 유저를 찾아 반환
- 송금 기능을 위해
GetCurrentUser()에는 현재 로그인한 유저 정보를 저장
public void TransferAmount()
{
string targetID = transferUserInput.text;
if (string.IsNullOrWhiteSpace(targetID))
{
ShowError("송금 대상을 입력하세요.");
return;
}
UserData sender = GameManager.Instance.GetCurrentUser();
UserData receiver = GameManager.Instance.FindUserID(targetID);
if (receiver == null)
{
ShowError("송금 대상이\n존재하지 않습니다.");
return;
}
if (string.IsNullOrWhiteSpace(transferAmountInput.text))
{
ShowError("송금 금액을 입력하세요.");
return;
}
if (!int.TryParse(transferAmountInput.text, out int amount) || amount <= 0)
{
ShowError("송금할 수 없습니다.");
return;
}
if (sender.balance < amount)
{
ShowError("잔액이 부족합니다.");
return;
}
if (targetID == sender.id)
{
ShowError("자신에게 송금할 수 없습니다.");
return;
}
sender.balance -= amount;
receiver.balance += amount;
if (GameManager.Instance.userInfo != null)
{
GameManager.Instance.userInfo.Refresh();
}
GameManager.Instance.SaveUserData();
Debug.Log($"송금 완료: {sender.name}이 {receiver.name}에게 {amount}원 송금");
transferUserInput.text = "";
transferAmountInput.text = "";
}
private void ShowError(string massage)
{
errorText.text = massage;
errorPanel.SetActive(true);
}
- 이후 PopupBank 스크립트에서 송금 기능을 구현
- 입력된 금액을 int.Tryparse로 반환해 주고,
GameManager.Instance.FindUserID(targetID)를 호출해서 송금할 대상을 찾음
- 잔액이 amount보다 크거나 작다면
sender.balance -= amount;으로 송금 처리
- 수신자에게는
receiver.balance += amount;로 잔액 추가
- 이후
GameManager.Instance.SaveUserData();를 통해 변경된 데이터를 저장