Go Version : 1.21.5 2024 컴퓨터공학과 캡스톤디자인 - Next Reality
이전 내용 : 2024-05-13-GoLang으로 UDP 서버 만들기 (2)
config 파일 생성
하드코딩 줄이고 추후에 변경 가능하게 하도록 config 파일에 포트같은걸 저장함
{
"MapServerURL" : "http://127.0.0.1:8070",
"MongoURL" : "mongodb://localhost:27017",
"GameServerPort" : "8050"}
그리고 main 부분에서 처음에 config 파일 읽어오도록 함
func LoadConfig(filename string) (Config, error) {
var config Config
file, err := os.Open(filename)
if err != nil {
return config, err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return config, err
}
err = json.Unmarshal(data, &config)
return config, err
}
Creator 권한 추가
많은 플레이어들이 무분별하게 오브젝트를 설치할 수 없도록 하기 위해서 Creator라는 권한을 생성함 이를 위해 맵 아이디 ←> 크리에이터 플레이어들의 아이디 쌍을 지음 아래는 크리에이터 리스트를 받아오는 함수
func GetCreatorList(db *mongo.Database) (map[string][]string, error) {
collection := db.Collection("creators")
cursor, err := collection.Find(context.TODO(), bson.D{})
if err != nil {
return nil, err
}
defer cursor.Close(context.TODO())
creatorMap := make(map[string][]string)
for cursor.Next(context.TODO()) {
var creatorList controller.CreatorLists
if err := cursor.Decode(&creatorList); err != nil {
log.Println("Decode error:", err)
continue
}
creatorMap[creatorList.MapId] = creatorList.Creators
}
if err := cursor.Err(); err != nil {
return nil, err
}
return creatorMap, nil
}
PlayerJoin 수정
id & 주소가 접속 플레이어 Map에 있을 때, 기존에는 무조건 들어올 수 없도록 했는데 만약 기존에 들어온 맵에 재 접속 하려고 시도하는 거라면 가능하도록 변경함
또한, id는 접속되어 있는데 주소가 다를 때 (다른 클라이언트로 접속 시도할 때) 이전에 접속한 사람을 끊어버리도록 PlayerLeave 커맨드를 가진 스키마를 생성해서 이전 주소로 보냄
MapReady 생성
MapReady는 플레이어가 맵에 접속했을 때 맵 서버로부터 오브젝트 배치 현황을 알 수 있는 맵 데이터를 성공적으로 수신했을 경우에 사용된다. 플레이어가 서버에 MapReady를 보냈다는 얘기는 해당 플레이어가 오브젝트의 모델 파일을 가지고 있다는 것을 의미하기 때문에, 서버에 기록된 오브젝트 이동 로그를 전송해줌으로써 오브젝트의 위치 변경을 점진적으로 볼 수 있도록 함.
순서는 다음과 같다.
- MapReady 커맨드가 포함된 스키마를 클라이언트로부터 수신함
- SendBeforeLog 함수가 실행되고, 플레이어가 접속하려는 맵이 마지막으로 저장된 시간을 확인
- 맵이 저장된 시간 이후로 기록된 로그를 찾는 FindDocumentAfterTime() 함수를 통해 로그를 찾음
- SendBeforeLog로 돌아와서, 3번을 통해 찾게된 로그를 하나하나 플레이어에게 송신함
func MapReady(conn *net.UDPConn, m ReceiveMessage, addr string) (bool, string) {
// MapReady 메시지 형태 :
// MapReady$sendUserId;SendTime;
if otherMessageLengthCheck(m.CommandName, len(m.OtherMessage)) {
_, isUserAddrExists := UserAddr[m.SendUserId]
if isUserAddrExists {
boolChan := make(chan bool)
go SendBeforeLog(conn, UserMapid[m.SendUserId], m.SendUserId, boolChan)
result := <-boolChan
if result {
return true, aurora.Sprintf(aurora.Green("Send After Log Complete"))
} else {
return false, aurora.Sprintf(aurora.Yellow("Error : Cannot Send After Log"))
}
} else {
return false, aurora.Sprintf(aurora.Yellow("Error : Cannot Found User [%s]"), m.SendUserId)
}
} else {
return false, aurora.Sprintf(aurora.Yellow("Error : MapReady Message Length Error : need 0, received %d"), len(m.OtherMessage))
}
}
func SendBeforeLog(conn *net.UDPConn, mapid string, userId string, result chan bool) {
defer func() {
if r := recover(); r != nil {
fmt.Println(aurora.Sprintf(aurora.Red("Recovered from panic: %v"), r))
result <- false
}
}()
mapSaveTimeString := controllerhttp.GetMapTime(mapid, MapServerURL)
mapSaveTime, err := time.Parse(time.RFC3339Nano, mapSaveTimeString.Message)
if err != nil {
fmt.Println(aurora.Sprintf(aurora.Red("Error in Parsing Saved Time : %s"), err))
result <- false
return
}
logResult, err := FindDocumentsAfterTime(mapSaveTime, mapid)
if err != nil {
fmt.Println(aurora.Sprintf(aurora.Red("Error in Load Log Result : %s"), err))
result <- false
return
}
udpAddr, err := net.ResolveUDPAddr("udp", UserAddr[userId])
if err != nil {
fmt.Println(aurora.Sprintf(aurora.Red("Error : Resolve UDP Address Error Occured.\nError Message : %s"), err))
result <- false
return
}
for _, logOne := range logResult {
_, err := conn.WriteToUDP([]byte(logOne.OriginalMessage+";s"), udpAddr)
if err != nil {
fmt.Println(aurora.Sprintf(aurora.Red("%s | SendBeforeLog -> UDP Error : %s (%s)"), time.Now().Format("2006-01-02 15:04:05.000"), err, logOne.OriginalMessage))
continue
}
time.Sleep(100 * time.Millisecond)
}
result <- true
}
func FindDocumentsAfterTime(parsedTimeFromMaptime time.Time, mapid string) ([]LogResponseData, error) {
collection := DBClient.Collection("log")
currentTime := time.Now().UTC()
filter := bson.M{
"timestamp": bson.M{
"$gt": parsedTimeFromMaptime,
"$lt": currentTime,
},
"mapid": mapid,
"$or": []bson.M{
{"originalmessage": bson.M{"$regex": "^AssetCreate"}},
{"originalmessage": bson.M{"$regex": "^AssetMove"}},
{"originalmessage": bson.M{"$regex": "^AssetDelete"}},
},
}
cursor, err := collection.Find(context.TODO(), filter)
if err != nil {
return nil, err
}
defer cursor.Close(context.TODO())
var results []LogResponseData
for cursor.Next(context.TODO()) {
var elem LogResponseData
err := cursor.Decode(&elem)
if err != nil {
fmt.Printf("Error : %s\n", err)
return nil, err
}
results = append(results, elem)
}
if err := cursor.Err(); err != nil {
fmt.Printf("Error : %s\n", err)
return nil, err
}
return results, nil
}
오브젝트 Lock 구현
만약 여러 사람이 동시에 한 에셋을 변경하려고 한다면 문제가 생길 것임. 따라서, 오브젝트에 대한 변경 독점권의 의미를 가지는 Lock을 구현함
func ItemLock(userId string, itemId string) {
mapId := UserMapid[userId]
_, lockedListResult := MapidLockedList[mapId]
// if !lockedListResult => 해당 방에 Lock된 아이템이 없음 => 새로 만들어줘야함
if !lockedListResult {
MapidLockedList[mapId] = []string{itemId}
} else { // 해당 방에 Lock 된 아이템이 있음 => 뒤에 append
MapidLockedList[mapId] = append(MapidLockedList[mapId], itemId)
}
LockObjUser[itemId] = userId // 해당 아이템이 userId에 의해 Lock 된 것임을 확인
}
func ItemUnlock(userId string, itemId string) {
mapId := UserMapid[userId]
lockedList := MapidLockedList[mapId]
var itemIndex int
for index, item := range lockedList {
if item == itemId {
itemIndex = index
}
}
if itemIndex != -1 {
lockedList = append(lockedList[:itemIndex], lockedList[itemIndex+1:]...)
MapidLockedList[mapId] = lockedList
}
delete(LockObjUser, itemId)
fmt.Printf("Item [%s] Unlocked\n", itemId)
}
func isLocked(userId string, itemId string) bool {
mapId := UserMapid[userId]
lockedList, lockedListResult := MapidLockedList[mapId]
if !lockedListResult {
return false
}
for _, item := range lockedList {
if item == itemId {
return true
}
}
return false
}
플레이어가 오브젝트를 선택했을 때 ⇒ AssetSelect 플레이어가 오브젝트 선택을 해제했을 때 ⇒ AssetDeselect
func AssetSelect(conn *net.UDPConn, m ReceiveMessage, addr string) (bool, string) {
// AssetSelect 형태 :
// AssetSelect$SendUserId;SendTime;ObjectId;
// otherMessage length : 1
// Lock 함. 이미 누가 Lock 해놨으면 false 날리기
// if Lock 해둔 사람이 나임 -> true
// if 이미 뭔가를 lock 한 사람이 다른 물체를 lock 함 -> 기존 물체 deselect 하고 새 물체 lock으로 해야함
if otherMessageLengthCheck(m.CommandName, len(m.OtherMessage)) {
if isUserExists(m.SendUserId, addr) {
if isCreator(m.SendUserId) {
itemId := m.OtherMessage[0]
if !isLocked(m.SendUserId, itemId) {
resultKey, resultBool := findKeyByValue(LockObjUser, m.SendUserId) // 값으로 키 찾기
if resultBool { // 이미 lock 한 오브젝트가 있으면 그걸 unlock 하는게 우선
// ItemUnlock(m.SendUserId, resultKey)
// AssetDeselect$SendUserId;SendTime;ObjectId;
unlockMessage := "AssetDeselect$" + m.SendUserId + ";" + strconv.FormatInt(time.Now().UnixMilli(), 10) + ";" + resultKey
structUnlockMessage, err := MessageParser(unlockMessage)
if !err {
fmt.Printf("leave message parsing error\n")
return false, aurora.Sprintf(aurora.Yellow("Error : Cannot Parsing Leave Message"))
}
logData := LogMessage{
Timestamp: time.Now().UTC(),
MapId: UserMapid[m.SendUserId],
Message: structUnlockMessage,
OriginalMessage: unlockMessage,
}
_, insertErr := DBClient.Collection("log").InsertOne(context.TODO(), logData)
if insertErr != nil {
fmt.Printf("insert result : %s\n", insertErr)
}
unlockResult, unlockResultText := AssetDeselect(conn, structUnlockMessage, addr)
if !unlockResult {
return false, unlockResultText
}
broadcast(conn, m.SendUserId, unlockMessage, true)
}
ItemLock(m.SendUserId, itemId)
// fmt.Printf("MapidLockedList : %v\n", MapidLockedList[UserMapid[m.SendUserId]])
// fmt.Printf("LockObjUser : %s\n", LockObjUser[itemId])
return true, aurora.Sprintf(aurora.Green("Success : User [%s] Asset [%s] Select\n"), m.SendUserId, itemId)
} else if isLocked(m.SendUserId, itemId) && LockObjUser[itemId] == m.SendUserId {
// fmt.Printf("MapidLockedList : %v\n", MapidLockedList[UserMapid[m.SendUserId]])
// fmt.Printf("LockObjUser : %s (Again)\n", LockObjUser[itemId])
return true, aurora.Sprintf(aurora.Gray(12, "Success : User[%s] Asset [%s] Select Again\n"), m.SendUserId, itemId)
}
return false, aurora.Sprintf(aurora.Yellow("Error : Item [%s] is Locked"), itemId)
}
return false, aurora.Sprintf(aurora.Yellow("Error : User [%s] is not Creator"), m.SendUserId)
} else {
return false, aurora.Sprintf(aurora.Yellow("Error : Cannot Found User [%s]"), m.SendUserId)
}
} else {
return false, aurora.Sprintf(aurora.Yellow("Error : AssetSelect Message Length Error : need 1, received %d"), len(m.OtherMessage))
}
}
func AssetDeselect(conn *net.UDPConn, m ReceiveMessage, addr string) (bool, string) {
// AssetDeselect 형태 :
// AssetDeselect$SendUserId;SendTime;ObjectId;
// otherMessage length : 1
// Unlock 함.
if otherMessageLengthCheck(m.CommandName, len(m.OtherMessage)) {
if isUserExists(m.SendUserId, addr) {
if isCreator(m.SendUserId) {
itemId := m.OtherMessage[0]
if isLocked(m.SendUserId, itemId) {
lockUser := LockObjUser[itemId]
if m.SendUserId == lockUser {
ItemUnlock(m.SendUserId, itemId)
return true, aurora.Sprintf(aurora.Green("Success : User [%s] Asset [%s] Deselect\n"), m.SendUserId, m.OtherMessage[0])
} else {
return false, aurora.Sprintf(aurora.Yellow("Error : Item [%s] is Locked\n"), itemId)
}
} else {
fmt.Printf("Error : User [%s] not lock Asset [%s]\n", m.SendUserId, itemId)
fmt.Printf("Map [%s] Lock List : %v \n", UserMapid[m.SendUserId], MapidLockedList[UserMapid[m.SendUserId]])
return false, aurora.Sprintf(aurora.Yellow("Error : Unavailable Access to Asset Deselect [%s]\n"), itemId)
}
}
return false, aurora.Sprintf(aurora.Yellow("Error : User [%s] is not Creator"), m.SendUserId)
} else {
return false, aurora.Sprintf(aurora.Yellow("Error : Cannot Found User [%s]"), m.SendUserId)
}
} else {
return false, aurora.Sprintf(aurora.Yellow("Error : AssetSelect Message Length Error : need 1, received %d"), len(m.OtherMessage))
}
}
또한 오브젝트를 삭제할 때에는 자동으로 Unlock 되도록 변경함
MapInit 작성
도합 80명 대상으로 진행한 테스트에서, 맵에 오브젝트가 너무 많을 때 일일히 삭제하는 것이 매우 번거롭다는 의견을 받음 따라서 아예 초기화 할 수 있도록 하는 스키마인 MapInit을 만듬
func MapInit(conn *net.UDPConn, m ReceiveMessage, addr string) (bool, string) {
// MapInit 형태 :
// MapInit$SendUserId;SendTime
// otherMessage length : 0
// 보낸 유저가 있는 유저고, Creator List에 있으면 return true
// 딱히 더 할 작업은 없음
if otherMessageLengthCheck(m.CommandName, len(m.OtherMessage)) {
if isUserExists(m.SendUserId, addr) {
if isCreator(m.SendUserId) {
return true, aurora.Sprintf(aurora.Green("Success : User [%s] Init Map [%s]\n"), m.SendUserId, UserMapid[m.SendUserId])
}
return false, aurora.Sprintf(aurora.Yellow("Error : User [%s] is not Creator"), m.SendUserId)
} else {
return false, aurora.Sprintf(aurora.Yellow("Error : Cannot Found User [%s]"), m.SendUserId)
}
} else {
return false, aurora.Sprintf(aurora.Yellow("Error : MapInit Message Length Error : need 0, received %d"), len(m.OtherMessage))
}
}