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를 보냈다는 얘기는 해당 플레이어가 오브젝트의 모델 파일을 가지고 있다는 것을 의미하기 때문에, 서버에 기록된 오브젝트 이동 로그를 전송해줌으로써 오브젝트의 위치 변경을 점진적으로 볼 수 있도록 함.

순서는 다음과 같다.

  1. MapReady 커맨드가 포함된 스키마를 클라이언트로부터 수신함
  2. SendBeforeLog 함수가 실행되고, 플레이어가 접속하려는 맵이 마지막으로 저장된 시간을 확인
  3. 맵이 저장된 시간 이후로 기록된 로그를 찾는 FindDocumentAfterTime() 함수를 통해 로그를 찾음
  4. 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))
	}
}