728x90
SMALL

서버와 클라이언트 통신을 위해 구현했던 ClientManager 게임에 적용하는 작업을 하였습니다.

 

TemplateManager 제거

TemplateService를 구현하면서 필요 없어진 TemplateManager와 리소스 폴더에 저장된 템플릿을 제거하였습니다.

 

로그인 임시 구현

Login 씬을 추가하여 이전에 만들었던 맵들과 함께 Build Settings에 등록하였습니다.

 

LoginLogic 스크립트를 추가하여 Login 씬에 추가하였습니다.
UI작업들은 나중에 할 계획이기 때문에 로그인 작업은 이걸로 마무리하고, 다음 작업을 시작하였습니다.

public class LoginLogic : MonoBehaviour
{
    IEnumerator Start()
    {
        ClientManager.CreateInstance();
        yield return new WaitForEndOfFrame();
        Loading();
    }

    async void Loading()
    {
        var cliMgr = ClientManager.Instance;

        if (!await cliMgr.TemplateService.LoadAll())
        {
            Debug.LogError("Load templates failed.");
            return;
        }

        var email = "test@test.com";
        var password = "12345678";

        var statusCode = await cliMgr.AuthService.Relogin();
        if (!statusCode.IsSuccess())
        {
            statusCode = await cliMgr.AuthService.Login(new()
            {
                Email = email,
                Password = password
            });
        }
        if (!statusCode.IsSuccess())
        {
            statusCode = await cliMgr.AuthService.CreateUser(new()
            {
                Email = email,
                Password = password
            });
        }
        if (!statusCode.IsSuccess())
        {
            Debug.LogError("Login and CreateUser failed.");
            return;
        }

        statusCode = await cliMgr.UserService.Load();
        if (!statusCode.IsSuccess())
        {
            Debug.LogError("Load user failed.");
            return;
        }

        GameLogic.LoadStage(cliMgr.UserService.User.CurrentStage.Index);
    }
}

 

게임 임시 구현

 

GameLogic 스크립트에서 스테이지 씬을 불러오면서 불러온 스테이지의 인덱스 번호도 저장할 수 있도록 LoadStage 메소드를 추가하였습니다.

public static void LoadStage(int index)
{
    stageIndex = index;
    var template = ClientManager.Instance.TemplateService.StageTemplates[index];
    SceneManager.LoadScene(template.SceneName);
}

 

GameLogic 스크립트에서 서버에서 받은 유저정보를 이용하여 자동차를 생성하도록 수정하였습니다.

IEnumerator Start()
{
    npcCarManager.Play();

    foreach (var trigger in customerTriggers)
    {
        trigger.OnPlayerEntered += (sender, args) =>
        {
            OnCarEnterTrigger(sender as CustomerTrigger);
        };
    }

    var carPrefab = ClientManager.Instance.UserService.User.CurrentCar.Prefab;
    if (carPrefab == null)
    {
        Debug.LogError("Player car prefab is null.");
        yield break;
    }

    PlayerCar = Instantiate(carPrefab)?.GetComponent<PlayerCar>();
    PlayerCar.SetPath(playerPath.path);
    PlayerCar.OnCrashed += (sender, args) =>
    {
        StartCoroutine(EndGame(false));
    };
    PlayerCar.OnArrive += (sender, args) =>
    {
        StartCoroutine(EndGame(true));
    };
    yield return new WaitForSeconds(1);
    PlayerCar.PlayMoving();
}

 

GameLogic 스크립트에서 토큰이 만료되었거나 서버와의 통신을 실패하였을 경우 Login 씬으로 다시 이동하도록 처리하였습니다.

void OnEnable()
{
    if (inputControls == null)
        inputControls = new();
    inputControls.Player.SetCallbacks(this);
    inputControls.Player.Enable();

    ClientManager.Instance.AuthService.TokenExpired += OnTokenExpired;
    ClientManager.Instance.UserService.UserUpdateFailed += OnUserUpdateFailed;
}

void OnDisable()
{
    inputControls?.Player.Disable();

    ClientManager.Instance.AuthService.TokenExpired -= OnTokenExpired;
    ClientManager.Instance.UserService.UserUpdateFailed -= OnUserUpdateFailed;
}

void OnTokenExpired(object sender, EventArgs args)
{
    Debug.LogError("Token was expired.");
    SceneManager.LoadScene(0);
}

void OnUserUpdateFailed(object sender, EventArgs args)
{
    Debug.LogError("User updating was failed.");
    SceneManager.LoadScene(0);
}

 

게임을 완료하면 보상을 획득하고, 다음 스테이지로 이동하도록 수정하였습니다.

async void EndGame(bool isGoal)
{
    PlayerCar.StopMoving();
    npcCarManager.Stop();

    var dt = DateTime.Now;

    if (isGoal)
        await ClientManager.Instance.UserService.EndStage(stageIndex, coin);

    var ts = DateTime.Now - dt;
    if (ts.TotalSeconds < 3)
        await UniTask.WaitForSeconds(3.0f - (float)ts.TotalSeconds, true);

    LoadStage(ClientManager.Instance.UserService.User.CurrentStage.Index);
}

 

이전에 자동차 프리팹을 만들었을 때 카메라를 추가 하지 않아 카메라를 추가하는 작업을 하였습니다.
모든 맵에 카메라를 추가하고, 각각의 자동차 프리팹은 Cinemachine에서 제공하는 가상 카메라를 추가하여 카메라가 플레이어를 따라다니도록 구현하였습니다.

 

버그 수정

위 작업들을 모두 끝내고 첫 테스트하였을 때 계속 버그가 발생하여 버그 수정작업을 하였습니다.

 

클라이언트

HttpContext 에서 발생하는 null 참조 에러를 수정하였습니다.

public Dictionary<string, string> Headers
{
    get;
    private set;
} = new();

 

UnityWebRequest 클래스의 Put 메소드를 사용할 때 헤더정보를 설정하지 않으면 JSON으로 데이터를 주고 받지 않기 때문에 application/json 추가하도록 수정하였습니다.

public async UniTask<HttpStatusCode> Put(string subUri, object data)
{
    using (var req = UnityWebRequest.Put($"{BaseUri}{subUri}", ToJsonSafety(data)))
    {
        foreach (var h in Headers)
            req.SetRequestHeader(h.Key, h.Value);
        req.SetRequestHeader("Content-Type", "application/json");
        await req.SendWebRequest();
        return (HttpStatusCode)req.responseCode;
    }
}

public async UniTask<(HttpStatusCode, T)> Put<T>(string subUri, object data)
{
    using (var req = UnityWebRequest.Put($"{BaseUri}{subUri}", ToJsonSafety(data)))
    {
        foreach (var h in Headers)
            req.SetRequestHeader(h.Key, h.Value);
        req.SetRequestHeader("Content-Type", "application/json");
        await req.SendWebRequest();
        return (
            (HttpStatusCode)req.responseCode,
            FromJsonSafety<T>(req.downloadHandler.text)
        );
    }
}

 

버그 수정 후 새로 테스트할 수 있도록 TemplateService에 저장된 버전을 리셋하는 메소드를 추가하였습니다.
추가한 메소드는 ClientManager에서 호출하도록 구현하였습니다.

public static void ResetTemplateVersions(string enviroment)
{
    PlayerPrefs.DeleteKey($"{enviroment}/TemplateVersions/Car");
    PlayerPrefs.DeleteKey($"{enviroment}/TemplateVersions/Stage");
}

 

TemplateService에 템플릿을 로컬에 처음 저장할 때, File.WriteAllText 에서 이미 열린파일에 접근하고 있다는 내용의 에러가 발생하여 수정하였습니다.

void SaveToLocal(string name, object content)
{
    try
    {
        var directory = $"{Application.persistentDataPath}/{enviroment}/Templates";
        if (!Directory.Exists(directory))
            Directory.CreateDirectory(directory);

        var path = $"{directory}/{name}";
        if (!File.Exists(path))
            File.Create(path).Close();

        File.WriteAllText(path, JsonConvert.SerializeObject(content));
    }
    catch (Exception ex)
    {
        Debug.LogException(ex);
    }
}

 

UserService 클래스의 EndStage 메소드는 다음 스테이지로 바꿔주는 과정을 작성하지 않아 작성하였습니다.

public async UniTask<HttpStatusCode> EndStage(int stageIndex, long coin)
{
    var currStageIndex = User.CurrentStage.Index;
    var stageCount = templateService.StageTemplates.Count;

    if (stageIndex < 0 || stageIndex >= stageCount)
    {
        Debug.LogError($"End stage failed. Because {stageIndex} is out of range.");
        return HttpStatusCode.BadRequest;
    }


    if (stageIndex > currStageIndex)
    {
        Debug.LogWarning($"End stage({stageIndex}) failed. Because {User.CurrentStage.Index} was not clear.");
        return HttpStatusCode.Forbidden;
    }

    if (coin >= templateService.StageTemplates[stageIndex].MaxCoin)
    {
        Debug.LogError($"End stage failed. Because {coin} is too much.");
        return HttpStatusCode.Forbidden;
    }

    if (stageIndex == currStageIndex && stageIndex < stageCount - 1)
        User.CurrentStage = templateService.StageTemplates[stageIndex + 1];
    User.Coin += coin;

    var res = await http.Put($"User/EndStage", new EndStageRequest
    {
        StageIndex = stageIndex,
        Coin = coin
    });
    if (!res.IsSuccess())
    {
        Debug.LogWarning($"End stage failed. - {res}");
        UserUpdateFailed?.Invoke(this, EventArgs.Empty);
    }
    return res;
}

 

서버

 

TemplateService에서 실수로 BsonSerializer를 이용하여 JSON 데이터를 클래스로 변환하도록 구현했기 때문에 JsonSerializer로 수정하였습니다.

public async Task<List<CarTemplate>?> GetCars()
{
    if (cars == null)
    {
        var model = await templates.Find(e => e.Name == "Car").FirstOrDefaultAsync();
        cars = JsonSerializer.Deserialize<List<CarTemplate>>(model.Datas.ToJson());
    }
    return cars;
}

public async Task<List<StageTemplate>?> GetStages()
{
    if (stages == null)
    {
        var model = await templates.Find(e => e.Name == "Stage").FirstOrDefaultAsync();
        stages = JsonSerializer.Deserialize<List<StageTemplate>>(model.Datas.ToJson());
    }
    return stages;
}

 

UserController에서 처음 시작하는 유저에게 자동차를 지급한 후 유저정보를 DB에 저장하지 않아 수정하였습니다.

[HttpGet]
[ProducesResponseType<UserResponse>(200)]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<ActionResult> Get()
{
    var userId = ClaimHelper.FindNameIdentifier(User);
    if (string.IsNullOrEmpty(userId))
        return Unauthorized();

    var user = await userRepository.Get(userId);
    if (user == null)
        return Forbid();

    // 자동차 지급
    if (user.Cars == null || user.Cars.Count == 0)
    {
        var cars = await templateService.GetCars();
        var newCar = cars?.FirstOrDefault(e => e.Cost == 0);
        if (newCar != null)
        {
            user.CurrentCarId = newCar?.Id;
            user.Cars = [ user.CurrentCarId! ];
            _ = userRepository.Update(userId, user);
        }
    }

    return Ok(new UserResponse
    {
        Nickname = user.Nickname,
        Coin = user.Coin,
        Cars = user.Cars,
        CurrentCar = user.CurrentCarId,
        CurrentStage = user.CurrentStageIndex
    });
}

 

UserController 클래스의 EndStage 메소드는 다음 스테이지로 바꿔주는 과정을 작성하지 않아 작성하였습니다.

[HttpPut("EndStage")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<ActionResult> EndStage([FromBody] EndStageRequest body)
{
    var userId = ClaimHelper.FindNameIdentifier(User);
    if (string.IsNullOrEmpty(userId))
        return Unauthorized();

    var user = await userRepository.Get(userId);
    if (user == null)
        return Forbid();

    var stageTemps = await templateService.GetStages();
    var stageCount = stageTemps?.Count ?? 0;
    var stageIndex = body.StageIndex;
    // 템플릿에 등록되지 않은 스테이지
    if (stageIndex < 0 || body.StageIndex >= stageCount)
        return BadRequest();

    // 현재 스테이지도 아직 클리어하지 않았을 경우
    if (stageIndex > user.CurrentStageIndex)
        return Forbid();

    // 클라이언트에서 계산한 요금이 서버에서 계산한 요금보다 더 큰 경우
    var maxCoin = stageTemps?[stageIndex]?.MaxCoin ?? 0;
    if (body.Coin >= maxCoin)
        return Forbid();

    // 다음 스테이지 개방
    if (stageIndex == user.CurrentStageIndex && stageIndex < stageCount + 1)
        user.CurrentStageIndex = stageIndex + 1;
    user.Coin += body.Coin;
    await userRepository.Update(user.Id!, user);

    return NoContent();
}

 

템플릿

 

xlsx 파일에 입력한 숫자들 중 일부는 JSON으로 변환할 문자열로 변환되는 현상이 발생하여 반드시 숫자로 변환해야 되는 데이터는 float 함수와 int 함수로 강제 형변환을 하도록 수정하였습니다.

def generate_car(file_path, sheet_name):
    book = load_workbook(file_path, data_only=True)
    sheet = book[sheet_name]
    skip = 0
    temp_group = []
    for row in sheet.rows:
        # 세번째 줄부터 데이터 저장
        if skip < 2:
            skip += 1
            continue      
        # ID컬럼이 비어 있으면 추가하지 않음
        if row[0].value == None:
            continue
        new_temp = {
            'Id': row[0].value,
            'Name': {
                'Table': row[1].value,
                'Key': row[2].value
            },
            'Icon': row[3].value,
            'Prefab': row[4].value,
            'Cost': int(row[5].value)
        }
        temp_group.append(new_temp)
    return temp_group



def generate_stage(file_path, sheet_name):
    book = load_workbook(file_path, data_only=True)
    sheet = book[sheet_name]
    skip = 0
    temp_group = []
    for row in sheet.rows:
        # 두번째 줄부터 데이터 저장
        if skip < 1:
            skip += 1
            continue  
        # ID컬럼이 비어 있으면 추가하지 않음
        if row[0].value == None:
            continue
        new_temp = {
            'Id': row[0].value,
            'Scene': row[1].value,
            'Distance': float(row[2].value),
            'FareRate': float(row[3].value)
        }
        temp_group.append(new_temp)
    return temp_group

 

다음에는 UI를 추가하여 직접 이메일을 입력하여 로그인을 하거나 새로운 자동차를 구매하고 선택하는 기능들을 구현하도록 하겠습니다.

 

깃 허브 저장소 : taxi-game-3d-unity

728x90
LIST

+ Recent posts