Unity 온라인 리더보드 튜토리얼

이 튜토리얼에서는 Unity에서 게임에 온라인 리더보드를 구현하는 방법을 보여 드리겠습니다.

이것은 이전 튜토리얼인 Unity PHP 및 MySQL을 사용한 로그인 시스템의 연속입니다.

순위표를 갖는 것은 게임에 경쟁력 수준을 추가하여 재생 가능성을 높이는 좋은 방법입니다.

이전과 마찬가지로 이 튜토리얼에는 PHP 및 MySQLi(MySQL의 향상된 버전)와 함께 cPanel이 있는 서버가 필요합니다.

부담 없이 저렴한 프리미엄 VPS 호스팅이나 더 저렴한 공유 호스팅 대안을 확인해 보세요.

그럼 계속하자!

기존 스크립트 수정

위의 튜토리얼을 따랐다면 이제 'SC_LoginSystem'라는 스크립트가 생겼을 것입니다. 우리는 리더보드 기능에 몇 가지 코드를 추가하여 구현할 것입니다.

  • 'SC_LoginSystem' 스크립트를 엽니다

먼저 필요한 변수를 추가하는 것부터 시작합니다.

    //Leaderboard
    Vector2 leaderboardScroll = Vector2.zero;
    bool showLeaderboard = false;
    int currentScore = 0; //It's recommended to obfuscate this value to protect against hacking (search 'obfuscation' on sharpcoderblog.com to learn how to do it)
    int previousScore = 0;
    float submitTimer; //Delay score submission for optimization purposes
    bool submittingScore = false;
    int highestScore = 0;
    int playerRank = -1;
    [System.Serializable]
    public class LeaderboardUser
    {
        public string username;
        public int score;
    }
    LeaderboardUser[] leaderboardUsers;

참고: currentScore 변수는 게임에서 플레이어 점수를 추적하는 데 사용됩니다. 이 값은 서버에 제출되어 데이터베이스에 저장됩니다. 해킹으로부터 보호하기 위해 해당 값을 난독화하는 것이 좋습니다.

다음으로 점수 제출 및 순위표 검색을 담당하는 열거자 2개를 추가합니다. 마지막 대괄호를 닫기 전에 스크립트 끝부분에 아래 코드를 추가하세요.

    //Leaderboard
    IEnumerator SubmitScore(int score_value)
    {
        submittingScore = true;

        print("Submitting Score...");

        WWWForm form = new WWWForm();
        form.AddField("email", userEmail);
        form.AddField("username", userName);
        form.AddField("score", score_value);

        using (UnityWebRequest www = UnityWebRequest.Post(rootURL + "score_submit.php", form))
        {
            yield return www.SendWebRequest();

            if (www.isNetworkError)
            {
                print(www.error);
            }
            else
            {
                string responseText = www.downloadHandler.text;

                if (responseText.StartsWith("Success"))
                {
                    print("New Score Submitted!");
                }
                else
                {
                    print(responseText);
                }
            }
        }

        submittingScore = false;
    }

    IEnumerator GetLeaderboard()
    {
        isWorking = true;

        WWWForm form = new WWWForm();
        form.AddField("email", userEmail);
        form.AddField("username", userName);

        using (UnityWebRequest www = UnityWebRequest.Post(rootURL + "leaderboard.php", form))
        {
            yield return www.SendWebRequest();

            if (www.isNetworkError)
            {
                print(www.error);
            }
            else
            {
                string responseText = www.downloadHandler.text;

                if (responseText.StartsWith("User"))
                {
                    string[] dataChunks = responseText.Split('|');
                    //Retrieve our player score and rank
                    if (dataChunks[0].Contains(","))
                    {
                        string[] tmp = dataChunks[0].Split(',');
                        highestScore = int.Parse(tmp[1]);
                        playerRank = int.Parse(tmp[2]);
                    }
                    else
                    {
                        highestScore = 0;
                        playerRank = -1;
                    }

                    //Retrieve player leaderboard
                    leaderboardUsers = new LeaderboardUser[dataChunks.Length - 1];
                    for(int i = 1; i < dataChunks.Length; i++)
                    {
                        string[] tmp = dataChunks[i].Split(',');
                        LeaderboardUser user = new LeaderboardUser();
                        user.username = tmp[0];
                        user.score = int.Parse(tmp[1]);
                        leaderboardUsers[i - 1] = user;
                    }
                }
                else
                {
                    print(responseText);
                }
            }
        }

        isWorking = false;
    }

다음은 리더보드 UI입니다. void OnGUI() 뒤에 아래 코드를 추가합니다.

    //Leaderboard
    void LeaderboardWindow(int index)
    {
        if (isWorking)
        {
            GUILayout.Label("Loading...");
        }
        else
        {
            GUILayout.BeginHorizontal();
            GUI.color = Color.green;
            GUILayout.Label("Your Rank: " + (playerRank > 0 ? playerRank.ToString() : "Not ranked yet"));
            GUILayout.Label("Highest Score: " + highestScore.ToString());
            GUI.color = Color.white;
            GUILayout.EndHorizontal();

            leaderboardScroll = GUILayout.BeginScrollView(leaderboardScroll, false, true);

            for (int i = 0; i < leaderboardUsers.Length; i++)
            {
                GUILayout.BeginHorizontal("box");
                if(leaderboardUsers[i].username == userName)
                {
                    GUI.color = Color.green;
                }
                GUILayout.Label((i + 1).ToString(), GUILayout.Width(30));
                GUILayout.Label(leaderboardUsers[i].username, GUILayout.Width(230));
                GUILayout.Label(leaderboardUsers[i].score.ToString());
                GUI.color = Color.white;
                GUILayout.EndHorizontal();
            }

            GUILayout.EndScrollView();
        }
    }

void OnGUI() 내부(닫는 대괄호 앞)에 아래 코드를 추가합니다.

        //Leaderboard
        if (showLeaderboard)
        {
            GUI.Window(1, new Rect(Screen.width / 2 - 300, Screen.height / 2 - 225, 600, 450), LeaderboardWindow, "Leaderboard");
        }
        if (!isLoggedIn)
        {
            showLeaderboard = false;
            currentScore = 0;
        }
        else
        {
            GUI.Box(new Rect(Screen.width / 2 - 65, 5, 120, 25), currentScore.ToString());

            if (GUI.Button(new Rect(5, 60, 100, 25), "Leaderboard"))
            {
                showLeaderboard = !showLeaderboard;
                if (!isWorking)
                {
                    StartCoroutine(GetLeaderboard());
                }
            }
        }

마지막으로 플레이어 점수가 변경되면 제출하는 코드를 포함하는 void Update()입니다. 모든 변수 뒤의 스크립트 시작 부분에 아래 코드를 추가합니다.

    //Leaderboard
    void Update()
    {
        if (isLoggedIn)
        {
            //Submit score if it was changed
            if (currentScore != previousScore && !submittingScore)
            {
                if(submitTimer > 0)
                {
                    submitTimer -= Time.deltaTime;
                }
                else
                {
                    previousScore = currentScore;
                    StartCoroutine(SubmitScore(currentScore));
                }
            }
            else
            {
                submitTimer = 3; //Wait 3 seconds when it's time to submit again
            }

            //**Testing** Increase score on key press
            if (Input.GetKeyDown(KeyCode.Q))
            {
                currentScore += 5;
            }
        }
    }

**테스트** 부분에 주목하세요. 플레이 가능한 게임이 없으므로 Q를 눌러 점수를 올리기만 하면 됩니다(점수 시스템이 있는 게임이 이미 있는 경우 나중에 제거할 수 있습니다. 예:.코인 +1포인트 모으기 등)

재생을 누르고 로그인하면 2개의 새로운 요소, 즉 'Leaderboard' 버튼과 화면 상단의 점수 값을 볼 수 있습니다.

이제 MySQL 테이블을 생성해 보겠습니다.

MySQL 테이블 생성

사용자 점수는 별도의 MySQL 테이블에 저장됩니다.

  • CPanel에 로그인
  • DATABASES 섹션에서 "phpMyAdmin"를 클릭하세요.

  • 이전 튜토리얼에서 생성한 데이터베이스를 클릭한 다음 SQL 탭을 클릭합니다.

  • 아래 코드를 쿼리 편집기에 붙여넣은 다음 "Go"
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";

--
-- Table structure for table `sc_user_scores`
--

CREATE TABLE `sc_user_scores` (
  `row_id` int(11) NOT NULL,
  `user_id` int(11) NOT NULL,
  `user_score` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

--
-- Indexes for table `sc_user_scores`
--
ALTER TABLE `sc_user_scores`
  ADD PRIMARY KEY (`row_id`),
  ADD UNIQUE KEY `user_id` (`user_id`);

--
-- AUTO_INCREMENT for table `sc_user_scores`
--
ALTER TABLE `sc_user_scores`
  MODIFY `row_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
COMMIT;

위의 쿼리는 기본 'sc_users' 테이블에 대한 참조로 user_id와 함께 가장 높은 점수를 저장하는 'sc_user_scores'이라는 새 테이블을 생성합니다.

마지막 부분은 서버 측 논리를 구현하는 것입니다.

서버측 로직 구현

서버측 로직은 점수 수신/저장 및 순위표 검색을 담당하는 PHP 스크립트로 구성됩니다.

첫 번째 스크립트는 score_submit.php입니다.

  • 새 PHP 스크립트를 만들고 그 안에 아래 코드를 붙여넣습니다.

점수_제출.php

<?php
	if(isset($_POST["email"]) && isset($_POST["username"]) && isset($_POST["score"])){
		$errors = array();
		
		$email = $_POST["email"];
		$username = $_POST["username"];
		$submitted_score = intval($_POST["score"]);
		$user_id = -1;
		$current_highscore = -1;

		//Connect to database
		require dirname(__FILE__) . '/database.php';
		
		//Check if the user already registered, retrieve its user_id and score value (if exist)
		if ($stmt = $mysqli_conection->prepare("SELECT u.user_id, 
			(SELECT user_score FROM sc_user_scores WHERE user_id = u.user_id LIMIT 1) as user_score 
			FROM sc_users u WHERE u.email = ? AND u.username = ? LIMIT 1")) {
			
			/* bind parameters for markers */
			$stmt->bind_param('ss', $email, $username);
				
			/* execute query */
			if($stmt->execute()){
				
				/* store result */
				$stmt->store_result();

				if($stmt->num_rows > 0){
				
					/* bind result variables */
					$stmt->bind_result($user_id_tmp, $score_tmp);

					/* fetch value */
					$stmt->fetch();
					
					$user_id = $user_id_tmp;
					$current_highscore = $score_tmp;

				}else{
					$errors[] = "User not found.";
				}
				
				/* close statement */
				$stmt->close();
				
			}else{
				$errors[] = "Something went wrong, please try again.";
			}
		}else{
			$errors[] = "Something went wrong, please try again.";
		}
		
		//Submit new score
		if(count($errors) == 0){
			if(is_null($current_highscore) || $submitted_score > $current_highscore){
				
				if(is_null($current_highscore)){
					//Insert new record
					if ($stmt = $mysqli_conection->prepare("INSERT INTO sc_user_scores (user_id, user_score) VALUES(?, ?)")) {
						
						/* bind parameters for markers */
						$stmt->bind_param('ii', $user_id, $submitted_score);
							
						/* execute query */
						if($stmt->execute()){
							
							/* close statement */
							$stmt->close();
							
						}else{
							$errors[] = "Something went wrong, please try again.";
						}
					}else{
						$errors[] = "Something went wrong, please try again.";
					}
				}else{
					//Update existing record
					if ($stmt = $mysqli_conection->prepare("UPDATE sc_user_scores SET user_score = ? WHERE user_id = ? LIMIT 1")) {
						
						/* bind parameters for markers */
						$stmt->bind_param('ii', $submitted_score, $user_id);
							
						/* execute query */
						if($stmt->execute()){
							
							/* close statement */
							$stmt->close();
							
						}else{
							$errors[] = "Something went wrong, please try again.";
						}
					}else{
						$errors[] = "Something went wrong, please try again.";
					}
				}
				
			}else{
				$errors[] = "Submitted score is lower than the current highscore, skipping...";
			}
		}
		
		if(count($errors) > 0){
			echo $errors[0];
		}else{
			echo "Success";
		}
	}else{
		echo "Missing data";
	}
?>

마지막 스크립트는 leaderboard.php입니다.

  • 새 PHP 스크립트를 만들고 그 안에 아래 코드를 붙여넣습니다.

리더보드.php

<?php
	//Retrieve our score along with leaderboard
	if(isset($_POST["email"]) && isset($_POST["username"])){
		$returnData = array();
		
		$email = $_POST["email"];
		$username = $_POST["username"];

		//Connect to database
		require dirname(__FILE__) . '/database.php';
		
		//Get our score and rank
		$returnData[] = "User";
		if ($stmt = $mysqli_conection->prepare("SELECT us.user_score,
			(SELECT COUNT(row_id) FROM sc_user_scores WHERE user_score >= us.user_score LIMIT 1) as rank
			FROM sc_user_scores us
			WHERE us.user_id = (SELECT user_id FROM sc_users WHERE email = ? AND username = ? LIMIT 1) LIMIT 1")) {
			
			/* bind parameters for markers */
			$stmt->bind_param('ss', $email, $username);
				
			/* execute query */
			if($stmt->execute()){
				
				/* store result */
				$stmt->store_result();

				if($stmt->num_rows > 0){
				
					/* bind result variables */
					$stmt->bind_result($score_tmp, $user_rank);

					/* fetch value */
					$stmt->fetch();
					
					//Append 
					$returnData[0] .= "," . $score_tmp . "," . $user_rank;

				}
				
				/* close statement */
				$stmt->close();
				
			}
		}
		
		//Get top 100 players
		if ($stmt = $mysqli_conection->prepare("SELECT u.username, us.user_score 
			FROM sc_users u RIGHT JOIN sc_user_scores us ON u.user_id = us.user_id 
			WHERE u.user_id IS NOT NULL ORDER BY us.user_score DESC LIMIT 100")) {
				
			/* execute query */
			if($stmt->execute()){
				
				$result = $stmt->get_result();

				while ($row = $result->fetch_assoc())
				{
					$returnData[] = $row["username"] . "," . $row["user_score"];
				}
				
				/* close statement */
				$stmt->close();
				
			}
		}
		
		//The returned string will use '|' symbol for separation between player data and ',' for separation inside the player data
		echo implode('|', $returnData);
	}else{
		echo "Missing data";
	}
?>
  • 이전 튜토리얼에서 PHP 스크립트를 업로드한 동일한 폴더에 Score_submit.php 및 Leaderboard.php를 모두 업로드합니다.

모든 것이 설정된 후 리더보드를 클릭하면 점수를 기준으로 상위 100명의 플레이어와 함께 점수/순위가 로드됩니다.

추천 기사
PHP 및 MySQL을 사용한 Unity 로그인 시스템
Unity의 Photon Fusion 2 소개
Unity에서 멀티플레이어 네트워크 게임 구축
멀티플레이어 데이터 압축 및 비트 조작
PUN 2로 멀티플레이어 자동차 게임 만들기
PUN 2 지연 보상
Unity, PUN 2 룸에 멀티플레이어 채팅 추가