1. 프로젝트 생성
서버는 아래 조건으로 만들기로 결정하였습니다.
- 프로젝트 템플릿 : ASP.NET Core 웹 API
- 프레임워크 : .NET 8.0
- 데이터 베이스 : 몽고DB
프로젝트를 만든 후 NuGet 패키지 매니저에서 MongoDB.Driver를 추가하였습니다.
2. 서버 구현
유저정보를 관리할 때 사용할 UserModel 클래스와 UserRepository 클래스를 미리 만들어 두었습니다.
public class UserModel
{
[BsonId]
public string? Id { get; set; }
public string? Nickname { get; set; }
[BsonElement("Device")]
[JsonPropertyName("Device")]
public string? DeviceId { get; set; }
public long Coin { get; set; }
public List<string>? Cars { get; set; }
[BsonElement("CurrentCar")]
[JsonPropertyName("CurrentCar")]
public string? CurrentCarId { get; set; }
[BsonElement("CurrentStage")]
[JsonPropertyName("CurrentStage")]
public int CurrentStageIndex { get; set; }
}
public class UserRepository
{
readonly IMongoCollection<UserModel> users;
public UserRepository(DatabaseContext context)
{
users = context.Users;
}
public async Task Create(UserModel model) =>
await users.InsertOneAsync(model);
public async Task<UserModel> Get(string id) =>
await users.Find(e => e.Id == id).FirstOrDefaultAsync();
public async Task<UserModel> FindByDevice(string deviceId) =>
await users.Find(e => e.DeviceId == deviceId).FirstOrDefaultAsync();
public async Task Update(string id, UserModel model) =>
await users.ReplaceOneAsync(e => e.Id == id, model);
public async Task Delete(string id) =>
await users.DeleteOneAsync(e => e.Id == id);
}
서버에서 DB에 템플릿 정보를 읽고, 쓰는데 사용할 모델 클래스들을 추가하였습니다.
public class TemplateVersionModel
{
[BsonId]
public string? Name { get; set; }
public BsonArray? Datas { get; set; }
}
public class TemplateVersionModel
{
[BsonId]
public string? Name { get; set; }
public ulong Version { get; set; }
}
- TemplateModel
- 템플릿 데이터 관리
- 실제 개발자가 업로드한 데이터들
- TemplateVersionModel
- 템플릿의 버전을 관리
- 개발자가 DB에 템플릿을 업로드할 때마다 버전을 증가 시킨다.
- 클라이언트에서는 템플릿을 버전을 비교하여 새로 받을지 말지 결정한다.
서버에서 처음 가입한 유저에게 차량 1대를 주거나 자동차의 가격과 유저가 가진 금액을 비교할 때 템플릿에 저장된 정보가 필요합니다.
그렇다고 클라이언트에서 요청을 할 때마다 서버에서 계속 DB와 통신을 하는 것은 좋지 않다고 생각하여 서버에서 템플릿 정보를 메모리에 따로 저장하고 있도록 구현하기로 하였습니다.
그래서 템플릿을 메모리에서 관리할 때 필요한 클래스들을 추가하였습니다.
public class CarTemplate
{
public string? Id { get; set; }
public int Cost { get; set; }
}
public class StageTemplate
{
public string? Id { get; set; }
}
public class TemplateService
{
readonly IMongoCollection<TemplateVersionModel> versions;
readonly IMongoCollection<TemplateModel> templates;
List<CarTemplate>? cars;
List<StageTemplate>? stages;
public TemplateService(DatabaseContext context)
{
versions = context.TemplateVersions;
templates = context.Templates;
}
public async Task<ulong> Update(string name, JsonArray datas)
{
switch (name)
{
case "Car":
cars = datas.Deserialize<List<CarTemplate>>();
break;
case "Stage":
stages = datas.Deserialize<List<StageTemplate>>();
break;
default:
return 0;
}
var verModel = await versions.Find(e => e.Name == name).FirstOrDefaultAsync();
if (verModel != null)
{
++verModel.Version;
await versions.ReplaceOneAsync(e => e.Name == name, verModel);
await templates.ReplaceOneAsync(e => e.Name == name, new()
{
Name = name,
Datas = BsonSerializer.Deserialize<BsonArray>(datas.ToJsonString())
});
return verModel.Version;
}
else
{
await versions.InsertOneAsync(new()
{
Name = name,
Version = 1
});
await templates.InsertOneAsync(new()
{
Name = name,
Datas = BsonSerializer.Deserialize<BsonArray>(datas.ToJsonString())
});
return 1;
}
}
public async Task Delete(string name)
{
await versions.DeleteOneAsync(e => e.Name == name);
await templates.DeleteOneAsync(e => e.Name == name);
}
public async Task<Dictionary<string, ulong>> GetVersions()
{
var models = await versions.Find(e => true).ToListAsync();
var result = new Dictionary<string, ulong>();
foreach (var e in models)
{
if (!string.IsNullOrEmpty(e.Name))
result.Add(e.Name, e.Version);
}
return result;
}
public async Task<JsonArray?> Get(string name)
{
var model = await templates.Find(e => e.Name == name).FirstOrDefaultAsync();
if (model == null)
return null;
return JsonSerializer.Deserialize<JsonArray>(model.Datas.ToJson());
}
public async Task<List<CarTemplate>> GetCars()
{
if (cars == null)
{
var model = await templates.Find(e => e.Name == "Car").FirstOrDefaultAsync();
cars = BsonSerializer.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 = BsonSerializer.Deserialize<List<StageTemplate>>(model.Datas.ToJson());
}
return stages;
}
}
서버에서 프리팹과 씬정보를 알고 있을 필요가 없기 때문에 CarTemplate 클래스와 StageTemplate 클래스는 서버에 필요없는 정보들은 정의하지 않았습니다.
TemplateService 클래스는 UserRepository 처럼 UserModel 하나만 관리하지 않고, 상대적으로 다양한 기능을 가지고 있기 때문에 Repository라고 작명하지 않았습니다.
클라이언트에서 서버와 통신할 수 있도록 TemplateController 클래스도 추가하였습니다.
[Route("[controller]")]
[ApiController]
public class TemplateController : ControllerBase
{
readonly TemplateService service;
public TemplateController(TemplateService service)
{
this.service = service;
}
[HttpGet("versions")]
public async Task<ActionResult> GetVersions()
{
var versions = await service.GetVersions();
return Ok(versions);
}
[HttpGet("{name}")]
public async Task<ActionResult> Get(string name)
{
var templates = await service.Get(name);
if (templates == null)
return NotFound();
return Ok(templates);
}
[HttpPut("{name}")]
public async Task<ActionResult> Update(string name, [FromBody] JsonArray datas)
{
var version = await service.Update(name, datas);
return Ok(new { Version = version } );
}
[HttpDelete("{name}")]
public async Task<ActionResult> Delete(string name)
{
await service.Delete(name);
return NoContent();
}
}
옛날에는 게임과 서버가 통신할 때 POST만 사용하도록 구현하였지만 이번에는 GET, PUT, DELETE도 함께 사용해 보려고 합니다.
JSON도 여태까지 Netwonsoft.Json을 사용해 왔지만 요즘은 MS에서 제공하는 JSON이 더 빠르다는 내용을 인터넷에서 확인하여 Netwonsoft.Json을 대신 사용하도록 설정하는 작업은 하지 않았습니다.
3. 템플릿 연동
파이썬을 이용하여 xlsx에 입력한 정보를 서버와 DB에 저장하는 기능을 구현하였습니다.
template_generator.py 를 만들어 xlsx 파일에 입력한 데이터를 딕셔너리로 변환하는 코드를 정리하였습니다.
from openpyxl import load_workbook
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
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': 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
}
temp_group.append(new_temp)
return temp_group
deploy-car-client.py, deploy-stage-client.py 는 template_generator.py 에서 변환한 데이터를 저장하도록 수정하였습니다.
import json
from template_generator import generate_stage
temp_group = generate_stage('./Template(Dev).xlsx', 'Car')
client_path = '../taxi-game-3d-client/Assets/_TaxiGame/Resources/Templates/Car.json'
with open(client_path, 'w', encoding='UTF-8-sig') as f:
json.dump(temp_group, f, ensure_ascii=False)
import json
from template_generator import generate_stage
temp_group = generate_stage('./Template(Dev).xlsx', 'Stage')
client_path = '../taxi-game-3d-client/Assets/_TaxiGame/Resources/Templates/Stage.json'
with open(client_path, 'w', encoding='UTF-8-sig') as f:
json.dump(temp_group, f, ensure_ascii=False)
deploy-car-server.py, deploy-stage-client.py 추가하여 서버에 변환된 데이터를 전달하는 기능을 구현하였습니다.
import requests
from template_generator import generate_car
temp_group = generate_car('./Template(Dev).xlsx', 'Car')
res = requests.put('https://localhost:7170/Template/Car', json=temp_group, verify=False)
print(res.status_code)
import requests
from template_generator import generate_stage
temp_group = generate_stage('./Template(Dev).xlsx', 'Stage')
res = requests.put('https://localhost:7170/Template/Stage', json=temp_group, verify=False)
print(res.status_code)
여러 번의 테스트와 수정을 통해 DB에 잘 저장된 것을 확인 하였습니다.
이번 작업은 아직 파이썬, MS에서 기본 제공하는 JSON, 몽고DB가 익숙하지 않아 삽질을 많이 했습니다.
다음에는 로그인, 로그아웃, 유저정보관리 등 기능을 구현하겠습니다.
깃 허브 저장소 : taxi-game-3d-unity
'개발노트 > Taxi Game 3D' 카테고리의 다른 글
Devlog) Taxi Game 3D) 13) 서버, 클라이언트 통신 구현 (0) | 2023.12.22 |
---|---|
Devlog) Taxi Game 3D) 12) 서버 구현2 (2) | 2023.12.20 |
Devlog) Taxi Game 3D) 10) 자동차 제작 (0) | 2023.12.13 |
Devlog) Taxi Game 3D) 9) 맵 제작 2 (2) | 2023.12.09 |
Devlog) Taxi Game 3D) 8) 맵 제작 1 (2) | 2023.12.04 |