에러를 해결했다
원인은 !!!
LocationUpdate와 LocationUpdatePayload를 햇갈려서 주고 받을때 문제가 발생한 것이였다.
아래 코드는 오늘 업데이트 된 코드 들이다!!!
// 위치 정보 업데이트 payload
message LocationUpdatePayload {
UserLocation user = 1; // 단일 사용자 위치 정보를 포함하는 필드
// 사용자 위치 정보를 정의하는 메시지
message UserLocation {
string id = 1; // 사용자 ID
float x = 2; // 사용자 X 좌표
float y = 3; // 사용자 Y 좌표
string status = 4; // 사용자 상태
}
}
더보기
// src/Codes/Spawner.cs
/**
* 사용자 오브젝트를 생성하고 관리하는 클래스입니다.
* 현재 활성화된 사용자 목록을 유지하며, 위치 업데이트를 처리합니다.
* 비활성화된 사용자를 오브젝트 풀에서 제거합니다.
*/
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class Spawner : MonoBehaviour
{
public static Spawner instance; // Singleton 패턴을 위한 인스턴스
void Awake() // MonoBehaviour의 Awake 메소드, 객체가 생성될 때 호출
{
instance = this; // 현재 인스턴스를 싱글톤 인스턴스로 설정
}
private Dictionary<string, PlayerPrefab> activeUsers = new Dictionary<string, PlayerPrefab>(); // 활성화된 사용자 목록
private Dictionary<string, float> lastUpdateTime = new Dictionary<string, float>(); // 각 사용자의 마지막 업데이트 시간
private const float USER_TIMEOUT = 5f; // 5초 동안 업데이트가 없으면 제거
// 위치 업데이트 데이터를 기반으로 사용자 오브젝트를 생성하거나 업데이트하는 메소드
public void Spawn(LocationUpdate data)
{
if (!GameManager.instance.isLive) // 게임이 활성화되어 있지 않으면 처리 중지
return;
float currentTime = Time.time; // 현재 시간 저장
// 새로운 위치 업데이트 처리
foreach (LocationUpdate.UserLocation user in data.users) // 위치 업데이트 데이터의 각 사용자에 대해 반복
{
if (user.id == GameManager.instance.deviceId) // 현재 디바이스의 사용자 ID는 무시
continue;
lastUpdateTime[user.id] = currentTime; // 해당 사용자의 마지막 업데이트 시간 갱신
// 활성 사용자 목록에서 기존 플레이어를 찾음
if (activeUsers.TryGetValue(user.id, out PlayerPrefab existingPlayer))
{
existingPlayer.UpdatePosition(user.x, user.y); // 기존 플레이어의 위치 업데이트
}
else // 새로운 플레이어인 경우
{
GameObject newPlayer = GameManager.instance.pool.Get(user); // 오브젝트 풀에서 사용자 오브젝트 요청
if (newPlayer != null) // 요청한 오브젝트가 유효한 경우
{
PlayerPrefab playerScript = newPlayer.GetComponent<PlayerPrefab>(); // PlayerPrefab 스크립트 가져오기
if (playerScript != null) // 스크립트가 유효한 경우
{
playerScript.UpdatePosition(user.x, user.y); // 새로운 플레이어의 위치 설정
activeUsers[user.id] = playerScript; // 활성 사용자 목록에 추가
}
}
}
}
// 일정 시간 동안 업데이트가 없는 유저만 제거
List<string> usersToRemove = activeUsers.Keys // 활성 사용자 목록의 키를 가져옴
.Where(id => currentTime - lastUpdateTime.GetValueOrDefault(id, 0) > USER_TIMEOUT) // 타임아웃 기준에 따라 필터링
.ToList(); // 리스트로 변환
// 타임아웃이 발생한 사용자 제거
foreach (string userId in usersToRemove)
{
Debug.Log($"Removing user {userId} due to timeout"); // 제거되는 사용자 로그 출력
if (activeUsers.TryGetValue(userId, out PlayerPrefab player)) // 사용자 목록에서 플레이어 찾기
{
GameManager.instance.pool.Remove(userId); // 오브젝트 풀에서 해당 사용자 제거
activeUsers.Remove(userId); // 활성 사용자 목록에서 제거
lastUpdateTime.Remove(userId); // 마지막 업데이트 시간 목록에서 제거
}
}
}
}
더보기
// src/Codes/PlayerPrefab.cs
/**
* 플레이어 프리팹의 애니메이션과 위치 업데이트를 관리하는 클래스입니다.
* 서버로부터 위치 정보를 수신하고, 애니메이션의 속성을 업데이트합니다.
* 디바이스 ID를 텍스트로 표시합니다.
*/
using TMPro; // TextMeshPro를 사용하기 위한 네임스페이스
using UnityEngine; // Unity 관련 기능을 사용하기 위한 네임스페이스
public class PlayerPrefab : MonoBehaviour
{
public RuntimeAnimatorController[] animCon; // 애니메이터 컨트롤러 배열
private Animator anim; // 애니메이터 컴포넌트
private SpriteRenderer spriter; // 스프라이트 렌더러
private Vector3 lastPosition; // 이전 위치
private Vector3 currentPosition; // 현재 위치
private Vector3 targetPosition; // 목표 위치
private Vector3 currentVelocity; // 현재 속도
private uint playerId; // 플레이어 ID
TextMeshPro myText; // 텍스트 메쉬 프로
[SerializeField] private float smoothTime = 0.1f; // 부드러운 이동을 위한 시간
private bool hasTarget = false; // 목표 위치가 설정되었는지 여부
private float lastUpdateTime; // 마지막 업데이트 시간
private const float UPDATE_THRESHOLD = 0.01f; // 업데이트 간격 기준
private const float MIN_MOVE_THRESHOLD = 0.001f; // 최소 이동 기준
// Awake 메소드, 객체가 생성될 때 호출
void Awake()
{
anim = GetComponent<Animator>(); // 애니메이터 컴포넌트 가져오기
spriter = GetComponent<SpriteRenderer>(); // 스프라이트 렌더러 가져오기
myText = GetComponentInChildren<TextMeshPro>(); // 자식에서 텍스트 메쉬 프로 가져오기
}
// 플레이어 초기화 메소드
public void Init(uint playerId, string id)
{
anim.runtimeAnimatorController = animCon[playerId]; // 애니메이터 컨트롤러 설정
lastPosition = transform.position; // 현재 위치를 이전 위치로 설정
currentPosition = transform.position; // 현재 위치 초기화
targetPosition = transform.position; // 목표 위치 초기화
this.playerId = playerId; // 플레이어 ID 설정
lastUpdateTime = Time.time; // 마지막 업데이트 시간 초기화
// 디바이스 ID를 텍스트로 설정
if (id.Length > 5)
{
myText.text = id[..5]; // ID가 5자 이상이면 잘라서 표시
}
else
{
myText.text = id; // ID가 5자 이하이면 그대로 표시
}
// 스프라이트의 색상 초기화
if (spriter != null)
{
spriter.color = new Color(1f, 1f, 1f, 1f); // 완전 불투명
}
myText.GetComponent<MeshRenderer>().sortingOrder = 6; // 텍스트의 정렬 순서 설정
}
// 활성화될 때 호출되는 메소드
void OnEnable()
{
// 애니메이터 컨트롤러 설정
if (anim != null && playerId < animCon.Length)
{
anim.runtimeAnimatorController = animCon[playerId];
}
// 스프라이트 색상 초기화
if (spriter != null)
{
spriter.color = new Color(1f, 1f, 1f, 1f); // 완전 불투명
}
}
// 위치 업데이트 메소드
public void UpdatePosition(float x, float y)
{
Vector3 newTargetPos = new Vector3(x, y); // 새로운 목표 위치 생성
// 너무 작은 움직임은 무시
if (hasTarget && Vector3.Distance(targetPosition, newTargetPos) < MIN_MOVE_THRESHOLD)
{
return; // 목표 위치가 너무 가까우면 종료
}
// 업데이트 간격이 너무 짧으면 스킵
float currentTime = Time.time; // 현재 시간 저장
if (currentTime - lastUpdateTime < UPDATE_THRESHOLD)
{
return; // 업데이트 간격이 짧으면 종료
}
lastPosition = currentPosition; // 이전 위치 업데이트
targetPosition = newTargetPos; // 목표 위치 업데이트
lastUpdateTime = currentTime; // 마지막 업데이트 시간 갱신
hasTarget = true; // 목표 위치가 설정되었음을 표시
}
// 매 프레임 호출되는 Update 메소드
void Update()
{
// 게임이 진행 중이지 않거나 목표가 없으면 종료
if (!GameManager.instance.isLive || !hasTarget)
return;
float deltaTime = Time.deltaTime; // 프레임 간 시간 차이 저장
// 부드러운 이동 처리
currentPosition = Vector3.SmoothDamp(
currentPosition, // 현재 위치
targetPosition, // 목표 위치
ref currentVelocity, // 현재 속도 참조
smoothTime, // 부드러운 이동 시간
Mathf.Infinity, // 최대 속도
deltaTime // 프레임 간 시간 차이
);
// 위치가 충분히 변경되었을 때만 업데이트
if (Vector3.Distance(transform.position, currentPosition) > MIN_MOVE_THRESHOLD)
{
transform.position = currentPosition; // 오브젝트 위치 업데이트
UpdateAnimation(deltaTime); // 애니메이션 업데이트
}
}
// LateUpdate 메소드, Update 이후 호출
void LateUpdate()
{
// 게임이 진행 중이지 않으면 종료
if (!GameManager.instance.isLive)
return;
// 목표 위치에 충분히 가까워졌을 때
if (hasTarget && Vector3.Distance(currentPosition, targetPosition) < MIN_MOVE_THRESHOLD)
{
currentPosition = targetPosition; // 현재 위치를 목표 위치로 설정
transform.position = currentPosition; // 오브젝트 위치 업데이트
currentVelocity = Vector3.zero; // 속도 초기화
anim.SetFloat("Speed", 0); // 애니메이션 속도 초기화
}
}
// 애니메이션 업데이트 메소드
private void UpdateAnimation(float deltaTime)
{
if (anim == null || spriter == null) return; // 애니메이터나 스프라이트가 없으면 종료
Vector2 moveDirection = targetPosition - lastPosition; // 이동 방향 계산
float speed = currentVelocity.magnitude; // 현재 속도 계산
// 부드러운 애니메이션 전환
float currentSpeed = anim.GetFloat("Speed"); // 현재 애니메이션 속도 가져오기
float targetSpeed = speed; // 목표 애니메이션 속도 설정
float smoothSpeed = Mathf.Lerp(currentSpeed, targetSpeed, deltaTime * 10f); // 부드러운 속도 전환
anim.SetFloat("Speed", smoothSpeed); // 애니메이션 속도 설정
// 이동 방향에 따라 스프라이트 반전
if (Mathf.Abs(moveDirection.x) > MIN_MOVE_THRESHOLD)
{
spriter.flipX = moveDirection.x < 0; // 왼쪽으로 이동할 경우 스프라이트 반전
}
}
// 비활성화될 때 호출되는 메소드
void OnDisable()
{
hasTarget = false; // 목표가 없음을 표시
currentVelocity = Vector3.zero; // 현재 속도 초기화
}
}
더보기
// src/Codes/Packets.cs
/**
* 패킷을 직렬화하고 역직렬화하는 기능을 제공하는 클래스입니다.
* 다양한 패킷 유형과 페이로드에 대한 정의를 포함하고 있습니다.
*/
using UnityEngine; // Unity 관련 기능을 사용하기 위한 네임스페이스
using ProtoBuf; // Protobuf 직렬화를 사용하기 위한 네임스페이스
using System.IO; // 파일 및 스트림 작업을 위한 네임스페이스
using System.Buffers; // 버퍼 작성을 위한 네임스페이스
using System.Collections.Generic; // 컬렉션을 사용하기 위한 네임스페이스
using System; // 기본 시스템 기능을 위한 네임스페이스
public class Packets : MonoBehaviour
{
// 패킷 유형을 정의하는 열거형
public enum PacketType { Ping, Normal, Location = 3 } // Ping, Normal, Location 패킷 유형
// 핸들러 ID를 정의하는 열거형
public enum HandlerIds
{
Init = 0, // 초기화 핸들러 ID
LocationUpdate = 2 // 위치 업데이트 핸들러 ID
}
// 직렬화 메서드
public static void Serialize<T>(IBufferWriter<byte> writer, T data)
{
Serializer.Serialize(writer, data); // Protobuf를 사용하여 데이터를 직렬화
}
// 역직렬화 메서드
public static T Deserialize<T>(byte[] data)
{
try
{
using (var stream = new MemoryStream(data)) // 메모리 스트림 생성
{
return ProtoBuf.Serializer.Deserialize<T>(stream); // Protobuf를 사용하여 데이터를 역직렬화
}
}
catch (Exception ex)
{
Debug.LogError($"Deserialize: Failed to deserialize data. Exception: {ex}"); // 오류 로그 출력
throw; // 예외 재던지기
}
}
}
// 초기화 페이로드를 정의하는 클래스
[ProtoContract]
public class InitialPayload
{
[ProtoMember(1, IsRequired = true)]
public string deviceId { get; set; } // 디바이스 ID
[ProtoMember(2, IsRequired = true)]
public uint playerId { get; set; } // 플레이어 ID
[ProtoMember(3, IsRequired = true)]
public float latency { get; set; } // 지연 시간
}
// 공통 패킷을 정의하는 클래스
[ProtoContract]
public class CommonPacket
{
[ProtoMember(1)]
public uint handlerId { get; set; } // 핸들러 ID
[ProtoMember(2)]
public string userId { get; set; } // 사용자 ID
[ProtoMember(3)]
public string version { get; set; } // 버전 정보
[ProtoMember(4)]
public uint sequence { get; set; } // 시퀀스 번호
[ProtoMember(5)]
public byte[] payload { get; set; } // 패킷 데이터
}
// 위치 업데이트 페이로드를 정의하는 클래스
[ProtoContract]
public class LocationUpdatePayload
{
[ProtoMember(1)]
public List<UserLocation> users { get; set; } // 사용자 위치 리스트
// JSON 직렬화를 위한 기본 생성자
public LocationUpdatePayload()
{
users = new List<UserLocation>(); // 사용자 위치 리스트 초기화
}
// 사용자 위치 정보를 정의하는 클래스
[ProtoContract]
public class UserLocation
{
[ProtoMember(1)]
public string id { get; set; } // 사용자 ID
[ProtoMember(2)]
public float x { get; set; } // X 좌표
[ProtoMember(3)]
public float y { get; set; } // Y 좌표
[ProtoMember(4)]
public string status { get; set; } // 사용자 상태
}
}
// 위치 업데이트를 정의하는 클래스
[ProtoContract]
public class LocationUpdate
{
[ProtoMember(1)]
public List<UserLocation> users { get; set; } // 사용자 위치 리스트
// 기본 생성자
public LocationUpdate()
{
users = new List<UserLocation>(); // 사용자 위치 리스트 초기화
}
// 사용자 위치 정보를 정의하는 클래스
[ProtoContract]
public class UserLocation
{
[ProtoMember(1)]
public string id { get; set; } // 사용자 ID
[ProtoMember(2)]
public float x { get; set; } // X 좌표
[ProtoMember(3)]
public float y { get; set; } // Y 좌표
[ProtoMember(4)]
public string status { get; set; } // 사용자 상태
[ProtoMember(5)]
public uint playerId { get; set; } // 플레이어 ID
[ProtoMember(6)] // 추가 필드
public long lastUpdateTime { get; set; } // 마지막 업데이트 시간
}
}
// 응답 패킷을 정의하는 클래스
[ProtoContract]
public class Response
{
[ProtoMember(1)]
public uint handlerId { get; set; } // 핸들러 ID
[ProtoMember(2)]
public uint responseCode { get; set; } // 응답 코드
[ProtoMember(3)]
public long timestamp { get; set; } // 타임스탬프
[ProtoMember(4)]
public byte[] data { get; set; } // 응답 데이터
}
더보기
//src/Codes/NetworkManager.cs
/* 네트워크를 통해 클라이언트와 서버 간의 연결을 관리하는 클래스입니다.
IP와 포트를 입력받아 서버에 연결하고, 데이터를 송수신합니다.
패킷의 생성 및 처리, 게임 시작 로직을 포함합니다. */
using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
public class NetworkManager : MonoBehaviour
{
public static NetworkManager instance; // 네트워크 매니저 인스턴스
public InputField ipInputField; // IP 주소를 입력받기 위한 입력 필드
public InputField portInputField; // 포트 번호를 입력받기 위한 입력 필드
public InputField deviceIdInputField; // 디바이스 ID를 입력받기 위한 입력 필드
public GameObject uiNotice; // 사용자에게 알림을 표시하기 위한 UI 오브젝트
private TcpClient tcpClient; // TCP 클라이언트 객체
private NetworkStream stream; // 네트워크 통신을 위한 스트림
WaitForSecondsRealtime wait; // 대기 시간을 설정하기 위한 변수
private byte[] receiveBuffer = new byte[4096]; // 수신 데이터를 저장하기 위한 버퍼
private List<byte> incompleteData = new List<byte>(); // 불완전한 데이터를 저장하기 위한 리스트
private bool isReconnecting = false; // 재연결 여부를 나타내는 플래그
private int maxReconnectAttempts = 3; // 최대 재연결 시도 횟수
private float reconnectDelay = 5f; // 재연결 지연 시간 (초)
private int currentReconnectAttempt = 0; // 현재 재연결 시도 횟수
private float locationUpdateInterval = 1.0f; // 위치 업데이트 간격 (초)
private float lastUpdateTime = 0f; // 마지막 업데이트 시간
private uint currentSequence = 0; // 시퀀스 번호 카운터
// 시퀀스 번호를 증가시키고 반환하는 메서드
private uint GetNextSequence()
{
return ++currentSequence; // 시퀀스 값을 증가시키고 반환
}
private Queue<PacketData> packetQueue = new Queue<PacketData>(); // 패킷 데이터를 저장하기 위한 큐
private object queueLock = new object(); // 큐 접근을 위한 잠금 객체
// 패킷 데이터 구조체
private struct PacketData
{
public Packets.PacketType Type { get; set; } // 패킷 타입
public byte[] Data { get; set; } // 패킷 데이터
}
private Dictionary<string, ClientState> connectedClients = new Dictionary<string, ClientState>(); // 연결된 클라이언트 상태를 저장하는 딕셔너리
// 각 클라이언트의 상태를 나타내는 클래스
private class ClientState
{
public Vector2 lastPosition; // 마지막 위치
public long lastUpdateTime; // 마지막 업데이트 시간
public bool isActive; // 클라이언트의 활성 상태 여부
}
// MonoBehaviour의 Awake 메소드, 객체가 생성될 때 호출
void Awake()
{
instance = this; // 네트워크 매니저 인스턴스를 초기화
wait = new WaitForSecondsRealtime(5); // 5초 대기 시간 설정
}
void Update()
{
// 게임이 활성화되어 있고 TCP 클라이언트가 연결되어 있는 경우
if (GameManager.instance.isLive && tcpClient.Connected)
{
// 정기적으로 모든 클라이언트의 위치 정보 전송
if (Time.time - lastUpdateTime >= locationUpdateInterval) // 지정된 업데이트 간격이 지났는지 확인
{
// 현재 플레이어의 위치를 서버에 전송
SendLocationUpdatePacket(GameManager.instance.player.transform.position.x,
GameManager.instance.player.transform.position.y);
lastUpdateTime = Time.time; // 마지막 업데이트 시간을 현재 시간으로 갱신
}
}
// 큐에 있는 패킷 처리
while (true) // 무한 루프를 통해 큐에 있는 모든 패킷을 처리
{
PacketData packet; // 패킷 데이터 구조체
lock (queueLock) // 큐에 대한 동기화 잠금
{
// 큐가 비어있다면 루프 종료
if (packetQueue.Count == 0)
break;
// 큐에서 패킷을 꺼내기
packet = packetQueue.Dequeue();
}
try
{
// 패킷 타입에 따라 처리
switch (packet.Type)
{
case Packets.PacketType.Normal:
HandleNormalPacket(packet.Data); // 일반 패킷 처리
break;
case Packets.PacketType.Location:
HandleLocationPacket(packet.Data); // 위치 패킷 처리
break;
case Packets.PacketType.Ping:
HandlePingPacket(packet.Data); // 핑 패킷 처리
break;
default:
// 알 수 없는 패킷 타입 경고 로그 출력
Debug.LogWarning($"Unknown packet type: {packet.Type}");
break;
}
}
catch (Exception e)
{
// 패킷 처리 중 오류가 발생한 경우 오류 로그 출력
Debug.LogError($"Error processing packet: {e.Message}");
}
}
}
// 핑 패킷을 처리하는 메소드
private void HandlePingPacket(byte[] data)
{
Debug.Log("Ping packet received."); // 핑 패킷 수신 로그 출력
}
// 시작 버튼 클릭 시 호출되는 메소드
public void OnStartButtonClicked()
{
// 입력 필드에서 IP와 포트 정보를 가져옴
string ip = ipInputField.text;
string port = portInputField.text;
// 포트가 유효한지 확인
if (IsValidPort(port))
{
int portNumber = int.Parse(port); // 포트 번호 정수로 변환
// 디바이스 ID 입력 필드가 비어있지 않은 경우
if (deviceIdInputField.text != "")
{
GameManager.instance.deviceId = deviceIdInputField.text; // 입력된 디바이스 ID 설정
}
// 디바이스 ID가 비어있는 경우 고유 ID 생성
else if (GameManager.instance.deviceId == "")
{
GameManager.instance.deviceId = GenerateUniqueID(); // 고유 ID 생성
}
// 연결 시도 로그 출력
Debug.Log($"Connecting with DeviceId: {GameManager.instance.deviceId}");
// 서버에 연결
if (ConnectToServer(ip, portNumber))
{
StartGame(); // 게임 시작
}
else
{
// 연결 실패 시 효과음 재생 및 알림 표시
AudioManager.instance.PlaySfx(AudioManager.Sfx.LevelUp);
StartCoroutine(NoticeRoutine(1)); // 알림 루틴 시작
}
}
else
{
// 포트가 유효하지 않은 경우
AudioManager.instance.PlaySfx(AudioManager.Sfx.LevelUp);
StartCoroutine(NoticeRoutine(0)); // 알림 루틴 시작
}
}
// IP 유효성 검사 메소드
bool IsValidIP(string ip)
{
// 간단한 IP 유효성 검사: IP 주소 형식이 올바른지 확인
return System.Net.IPAddress.TryParse(ip, out _);
}
// 포트 유효성 검사 메소드
bool IsValidPort(string port)
{
// 간단한 포트 유효성 검사 (0 - 65535)
if (int.TryParse(port, out int portNumber)) // 포트 문자열을 정수로 변환
{
return portNumber > 0 && portNumber <= 65535; // 유효한 포트 범위인지 확인
}
return false; // 변환 실패 시 false 반환
}
// 서버에 연결하는 메소드
bool ConnectToServer(string ip, int port)
{
try
{
tcpClient = new TcpClient(ip, port); // TCP 클라이언트 생성 및 서버에 연결
stream = tcpClient.GetStream(); // 네트워크 스트림 가져오기
Debug.Log($"Connected to {ip}:{port}"); // 연결 성공 메시지 출력
return true; // 연결 성공
}
catch (SocketException e) // 소켓 예외 발생 시
{
HandleError("Failed to connect to server", e); // 에러 메시지 처리
return false; // 연결 실패
}
}
// 고유 ID 생성 메소드
string GenerateUniqueID()
{
return System.Guid.NewGuid().ToString(); // 고유 ID 생성
}
// 게임 시작 메소드
void StartGame()
{
// 게임 시작 코드 작성
Debug.Log("Game Started"); // 게임 시작 메시지 출력
StartReceiving(); // 데이터 수신 시작
SendInitialPacket(); // 초기 패킷 전송
var player = GameManager.instance.player; // 현재 플레이어 객체 가져오기
SendLocationUpdatePacket(player.transform.position.x, player.transform.position.y); // 플레이어 위치 업데이트 전송
}
// 알림 UI를 표시하고 숨기는 루틴
IEnumerator NoticeRoutine(int index)
{
uiNotice.SetActive(true); // 알림 UI 활성화
uiNotice.transform.GetChild(index).gameObject.SetActive(true); // 특정 알림 표시
yield return wait; // 대기
uiNotice.SetActive(false); // 알림 UI 비활성화
uiNotice.transform.GetChild(index).gameObject.SetActive(false); // 특정 알림 숨김
}
// 바이트 배열을 빅 엔디안으로 변환하는 메소드
public static byte[] ToBigEndian(byte[] bytes)
{
if (BitConverter.IsLittleEndian) // 현재 시스템이 리틀 엔디안인 경우
{
Array.Reverse(bytes); // 바이트 배열을 역순으로 변환
}
return bytes; // 변환된 배열 반환
}
// 패킷 헤더 생성 메소드
byte[] CreatePacketHeader(int dataLength, Packets.PacketType packetType)
{
// 헤더 길이(4) + 패킷 타입(1) + 데이터 길이
int packetLength = 4 + 1 + dataLength; // 전체 패킷 길이 계산
byte[] header = new byte[5]; // 헤더 배열 생성
// 패킷 길이를 빅 엔디안으로 변환
byte[] lengthBytes = BitConverter.GetBytes(packetLength); // 패킷 길이 바이트 배열 생성
lengthBytes = ToBigEndian(lengthBytes); // 빅 엔디안으로 변환
Array.Copy(lengthBytes, 0, header, 0, 4); // 길이 바이트 복사
// 패킷 타입 설정
header[4] = (byte)packetType; // 패킷 타입 추가
return header; // 생성된 헤더 반환
}
// 패킷 전송 메소드
async void SendPacket<T>(T payload, uint handlerId)
{
var payloadWriter = new ArrayBufferWriter<byte>(); // 바이트 배열 작성기 생성
Packets.Serialize(payloadWriter, payload); // 페이로드 직렬화
byte[] payloadData = payloadWriter.WrittenSpan.ToArray(); // 직렬화된 데이터 배열로 변환
// 공통 패킷 생성
CommonPacket commonPacket = new CommonPacket
{
handlerId = handlerId, // 핸들러 ID 설정
userId = GameManager.instance.deviceId, // 사용자 ID 설정
version = GameManager.instance.version, // 버전 설정
sequence = 0, // 시퀀스 번호 관리 필요
payload = payloadData // 페이로드 추가
};
var commonPacketWriter = new ArrayBufferWriter<byte>(); // 공통 패킷 작성기 생성
Packets.Serialize(commonPacketWriter, commonPacket); // 공통 패킷 직렬화
byte[] data = commonPacketWriter.WrittenSpan.ToArray(); // 직렬화된 데이터 배열로 변환
// 디버그용 로그
Debug.Log($"Sending packet - HandlerId: {handlerId}, UserId: {GameManager.instance.deviceId}, PayloadLength: {payloadData.Length}");
// 패킷 헤더 생성
byte[] header = CreatePacketHeader(data.Length, Packets.PacketType.Normal); // 패킷 헤더 생성
byte[] packet = new byte[header.Length + data.Length]; // 전체 패킷 배열 생성
Array.Copy(header, 0, packet, 0, header.Length); // 헤더 복사
Array.Copy(data, 0, packet, header.Length, data.Length); // 데이터 복사
await Task.Delay(GameManager.instance.latency); // 지연 시간 대기
stream.Write(packet, 0, packet.Length); // 패킷 전송
}
// 위치 업데이트 패킷 전송 메소드
public void SendLocationUpdatePacket(float x, float y)
{
if (!tcpClient.Connected) // 클라이언트가 연결되어 있지 않은 경우
{
Debug.LogWarning("Cannot send location update: Not connected"); // 경고 메시지 출력
return; // 메소드 종료
}
try
{
// 서버 좌표로 변환
float serverX = ConvertToServerX(x);
float serverY = ConvertToServerY(y);
// 위치 업데이트 객체 생성
LocationUpdate locationUpdate = new LocationUpdate();
locationUpdate.users.Add(new LocationUpdate.UserLocation
{
id = GameManager.instance.deviceId, // 사용자 ID 설정
playerId = GameManager.instance.playerId, // playerId 추가
x = serverX, // 변환된 X 좌표 설정
y = serverY, // 변환된 Y 좌표 설정
status = "active", // 사용자 상태 설정
lastUpdateTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() // 마지막 업데이트 시간 설정
});
// 위치 전송 로그 출력
Debug.Log($"Sending position - Unity: ({x}, {y}), Server: ({serverX}, {serverY})");
SendPacket(locationUpdate, (uint)Packets.HandlerIds.LocationUpdate); // 위치 업데이트 패킷 전송
}
catch (Exception e)
{
// 오류 발생 시 로그 출력
Debug.LogError($"Error sending location update: {e.Message}\n{e.StackTrace}");
}
}
// 좌표 변환 메서드들
private float ConvertToUnityX(float serverX)
{
return serverX * GameManager.instance.gridSize; // 서버 X 좌표를 Unity X 좌표로 변환
}
private float ConvertToServerX(float unityX)
{
return unityX / GameManager.instance.gridSize; // Unity X 좌표를 서버 X 좌표로 변환
}
private float ConvertToUnityY(float serverY)
{
return -serverY * GameManager.instance.gridSize; // 서버 Y 좌표를 Unity Y 좌표로 반전하여 변환
}
private float ConvertToServerY(float unityY)
{
return -unityY / GameManager.instance.gridSize; // Unity Y 좌표를 서버 Y 좌표로 반전하여 변환
}
// 초기 패킷 전송 메소드
void SendInitialPacket()
{
// 초기 패킷 생성
InitialPayload initialPayload = new InitialPayload
{
deviceId = GameManager.instance.deviceId, // 디바이스 ID 설정
playerId = GameManager.instance.playerId, // 플레이어 ID 설정
latency = GameManager.instance.latency // 지연 시간 설정
};
// 초기 패킷 전송 로그 출력
Debug.Log($"Sending initial packet - DeviceId: {initialPayload.deviceId}, PlayerId: {initialPayload.playerId}");
SendPacket(initialPayload, (uint)Packets.HandlerIds.Init); // 초기 패킷 전송
}
// 데이터 수신 시작 메소드
void StartReceiving()
{
_ = ReceivePacketsAsync(); // 비동기 데이터 수신 시작
}
// 비동기 패킷 수신 메소드
async System.Threading.Tasks.Task ReceivePacketsAsync()
{
while (tcpClient.Connected) // TCP 클라이언트가 연결된 동안 반복
{
try
{
// 데이터 읽기
int bytesRead = await stream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length);
if (bytesRead > 0) // 읽은 바이트가 있을 경우
{
ProcessReceivedData(receiveBuffer, bytesRead); // 수신 데이터 처리
}
}
catch (Exception e)
{
HandleError("Error receiving data", e); // 수신 오류 메시지 처리
break; // 루프 종료
}
}
}
// 수신 데이터 처리 메소드
void ProcessReceivedData(byte[] data, int length)
{
try
{
// 수신한 데이터 추가
incompleteData.AddRange(data.AsSpan(0, length).ToArray());
// 패킷이 완전할 때까지 반복
while (incompleteData.Count >= 5)
{
// 패킷 길이 확인
byte[] lengthBytes = incompleteData.GetRange(0, 4).ToArray();
int packetLength = BitConverter.ToInt32(ToBigEndian(lengthBytes), 0); // 패킷 길이 변환
// 완전한 패킷을 수신할 때까지 대기
if (incompleteData.Count < packetLength)
{
return; // 패킷이 완전하지 않으면 종료
}
// 패킷 타입과 데이터 추출
Packets.PacketType packetType = (Packets.PacketType)incompleteData[4];
byte[] packetData = incompleteData.GetRange(5, packetLength - 5).ToArray();
// 처리된 데이터 제거
incompleteData.RemoveRange(0, packetLength);
// 패킷을 큐에 추가
lock (queueLock) // 큐에 대한 동기화 잠금
{
packetQueue.Enqueue(new PacketData
{
Type = packetType, // 패킷 타입
Data = packetData // 패킷 데이터
});
}
}
}
catch (Exception e)
{
Debug.LogError($"Error in ProcessReceivedData: {e.Message}"); // 처리 중 오류 발생 시 로그 출력
}
}
// 일반 패킷 처리 메소드
void HandleNormalPacket(byte[] packetData)
{
try
{
// 패킷 데이터 역직렬화
var response = Packets.Deserialize<Response>(packetData);
Debug.Log($"Received response - HandlerId: {response.handlerId}, ResponseCode: {response.responseCode}");
// 응답 코드가 0이 아닌 경우 경고 표시
if (response.responseCode != 0 && !uiNotice.activeSelf)
{
AudioManager.instance.PlaySfx(AudioManager.Sfx.LevelUp); // 효과음 재생
StartCoroutine(NoticeRoutine(2)); // 알림 루틴 시작
return; // 메소드 종료
}
// 응답 데이터가 있는 경우 처리
if (response.data != null && response.data.Length > 0)
{
if (response.handlerId == 0) // 초기 응답 처리
{
GameManager.instance.GameStart(); // 게임 시작
}
else
{
ProcessResponseData(response.data, response.handlerId); // 응답 데이터 처리
}
}
}
catch (Exception e)
{
Debug.LogError($"Error processing normal packet: {e.Message}"); // 오류 발생 시 로그 출력
}
}
// 응답 데이터 처리 메소드
void ProcessResponseData(byte[] data, uint handlerId)
{
try
{
// 데이터 문자열로 변환
string jsonString = Encoding.UTF8.GetString(data);
Debug.Log($"Processing response data for handlerId: {handlerId}, Data: {jsonString}");
switch (handlerId)
{
case (uint)Packets.HandlerIds.LocationUpdate: // 위치 업데이트 핸들러
var locationUpdate = JsonUtility.FromJson<LocationUpdatePayload>(jsonString); // JSON 변환
if (locationUpdate != null && locationUpdate.users != null && locationUpdate.users.Count > 0)
{
var user = locationUpdate.users[0]; // 첫 번째 사용자 정보
Debug.Log($"Received location update - X: {user.x}, Y: {user.y}, Status: {user.status}");
// 위치 업데이트 처리 로직 추가
}
break;
// 다른 핸들러 케이스 추가
}
}
catch (Exception e)
{
Debug.LogError($"Error processing response data: {e.Message}"); // 처리 중 오류 발생 시 로그 출력
}
}
// 위치 패킷 처리 메소드
void HandleLocationPacket(byte[] data)
{
try
{
// 위치 업데이트 패킷 역직렬화
LocationUpdate response = Packets.Deserialize<LocationUpdate>(data);
if (response.users != null && response.users.Count > 0) // 사용자 정보가 있는 경우
{
LocationUpdate convertedResponse = new LocationUpdate
{
users = new List<LocationUpdate.UserLocation>() // 사용자 위치 리스트 초기화
};
foreach (var user in response.users) // 각 사용자에 대해 반복
{
// Unity 좌표계로 변환
float unityX = ConvertToUnityX(user.x);
float unityY = ConvertToUnityY(user.y);
// 클라이언트 상태 업데이트
if (!connectedClients.ContainsKey(user.id))
{
connectedClients[user.id] = new ClientState(); // 새로운 클라이언트 상태 추가
}
var clientState = connectedClients[user.id];
clientState.lastPosition = new Vector2(unityX, unityY); // 마지막 위치 업데이트
clientState.lastUpdateTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); // 마지막 업데이트 시간
clientState.isActive = true; // 클라이언트 활성화
// 변환된 사용자 정보 추가
var convertedUser = new LocationUpdate.UserLocation
{
id = user.id,
x = unityX,
y = unityY,
status = user.status,
playerId = user.playerId,
lastUpdateTime = clientState.lastUpdateTime
};
convertedResponse.users.Add(convertedUser); // 변환된 사용자 리스트에 추가
Debug.Log($"User {user.id} - Server: ({user.x}, {user.y}), Unity: ({unityX}, {unityY}), Status: {user.status}, LastUpdate: {clientState.lastUpdateTime}");
}
// 비활성 클라이언트 처리
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
foreach (var client in connectedClients.ToList())
{
if (currentTime - client.Value.lastUpdateTime > 10000) // 10초 이상 업데이트가 없는 경우
{
client.Value.isActive = false; // 클라이언트 비활성화
// 비활성 상태의 클라이언트도 위치 정보에 포함
convertedResponse.users.Add(new LocationUpdate.UserLocation
{
id = client.Key,
x = client.Value.lastPosition.x,
y = client.Value.lastPosition.y,
status = "inactive", // 비활성 상태 설정
lastUpdateTime = client.Value.lastUpdateTime
});
}
}
Spawner.instance.Spawn(convertedResponse); // 사용자 위치 정보 스폰
}
}
catch (Exception e)
{
HandleError("Error processing location packet", e); // 오류 발생 시 처리
}
}
// 오류 처리 메소드
void HandleError(string message, Exception e = null)
{
// 네트워크 오류 메시지 로그 출력
Debug.LogError($"Network Error: {message}");
// 예외 정보가 주어진 경우 로그 출력
if (e != null)
{
Debug.LogError($"Exception: {e}");
}
// 효과음 재생
AudioManager.instance.PlaySfx(AudioManager.Sfx.LevelUp);
// 알림 루틴 시작
StartCoroutine(NoticeRoutine(2));
// TCP 클라이언트가 연결되어 있지 않고 재연결 중이 아닌 경우
if (tcpClient != null && !tcpClient.Connected && !isReconnecting)
{
// 재연결 시도 루틴 시작
StartCoroutine(TryReconnect());
}
}
// 재연결 시도 루틴
IEnumerator TryReconnect()
{
// 이미 재연결 중인 경우 루틴 종료
if (isReconnecting) yield break;
isReconnecting = true; // 재연결 중 상태 설정
currentReconnectAttempt = 0; // 현재 재연결 시도 횟수 초기화
// TCP 클라이언트가 연결되지 않았고 최대 재연결 시도 횟수에 도달하지 않은 경우
while (!tcpClient.Connected && currentReconnectAttempt < maxReconnectAttempts)
{
try
{
// 기존 TCP 클라이언트 닫기
if (tcpClient != null)
{
tcpClient.Close();
tcpClient = null;
}
currentReconnectAttempt++; // 재연결 시도 횟수 증가
Debug.Log($"Attempting to reconnect... Attempt {currentReconnectAttempt}/{maxReconnectAttempts}");
// 서버에 재연결 시도
if (ConnectToServer(ipInputField.text, int.Parse(portInputField.text)))
{
stream = tcpClient.GetStream(); // 네트워크 스트림 가져오기
StartReceiving(); // 데이터 수신 시작
SendInitialPacket(); // 초기 패킷 전송
isReconnecting = false; // 재연결 상태 해제
Debug.Log("Reconnection successful!"); // 재연결 성공 메시지 출력
yield break; // 루틴 종료
}
}
catch (Exception e)
{
// 재연결 시도 중 오류 발생 시 로그 출력
Debug.LogError($"Reconnection attempt failed: {e.Message}");
}
// 재연결 지연 시간 대기
yield return new WaitForSeconds(reconnectDelay);
}
// 재연결 시도 후 여전히 연결되지 않은 경우
if (!tcpClient.Connected)
{
Debug.LogError("Failed to reconnect after maximum attempts"); // 재연결 실패 메시지 출력
StartCoroutine(NoticeRoutine(1)); // 알림 루틴 시작
}
isReconnecting = false; // 재연결 상태 해제
}
// 애플리케이션 종료 시 호출되는 메소드
void OnApplicationQuit()
{
// TCP 클라이언트가 연결되어 있는 경우 클라이언트 닫기
if (tcpClient != null && tcpClient.Connected)
{
tcpClient.Close(); // 클라이언트 종료
}
}
}
더보기
// src/events/onEnd.js
/*
이 코드는 클라이언트 소켓 연결 종료 시 호출되는 함수를 정의합니다.
연결이 종료되면 로그를 출력하고, 해당 소켓에 대한 사용자 정보를 제거합니다.
사용자 세션 관리를 위한 'removeUser' 함수를 사용합니다.
*/
import { removeUser, getUserBySocket } from '../session/user.session.js'; // 사용자 세션에서 사용자 제거 함수
import { gameSessions } from '../session/sessions.js';
// 클라이언트 소켓 연결 종료 시 호출되는 함수
export const onEnd = (socket) => () => {
console.log('클라이언트 연결이 종료되었습니다.');
// 1. 먼저 해당 소켓의 유저 정보를 찾습니다
const user = getUserBySocket(socket);
if (user) {
// 2. 유저가 속한 게임을 찾습니다
const game = gameSessions.find(g => g.users.some(u => u.id === user.id));
if (game) {
// 3. 게임에서 유저를 제거합니다
game.removeUser(user.id);
console.log(`User ${user.id} removed from game ${game.id}`);
}
}
// 4. 마지막으로 세션에서 유저를 제거합니다
removeUser(socket);
};
더보기
// src/handlers/user/initial.handler.js
/*
이 코드는 초기 사용자 핸들러 함수를 정의합니다.
장치 ID를 통해 사용자를 조회하고, 존재하지 않으면 새로 생성하며,
사용자 정보를 응답으로 클라이언트에 전송합니다. 오류 발생 시 적절히 처리합니다.
*/
import { addUser, getUserById, removeUser } from '../../session/user.session.js'; // 사용자 세션에 사용자 추가, 조회 및 제거 함수 임포트
import { userSessions } from '../../session/sessions.js'; // 현재 사용자 세션 정보 임포트
import { HANDLER_IDS, RESPONSE_SUCCESS_CODE } from '../../constants/handlerIds.js'; // 핸들러 ID 및 응답 코드 상수 임포트
import { createResponse } from '../../utils/response/createResponse.js'; // 응답 생성 유틸리티 임포트
import { handleError } from '../../utils/error/errorHandler.js'; // 오류 처리 유틸리티 임포트
import { createUser, findUserByDeviceID, updateUserLogin } from '../../db/user/user.db.js'; // 데이터베이스 사용자 관련 함수 임포트
import { getAllGameSessions } from '../../session/game.session.js'; // 모든 게임 세션 정보를 가져오는 함수 임포트
// 초기 사용자 핸들러 함수 정의
const initialHandler = async ({ socket, userId, payload }) => {
try {
// 페이로드에서 deviceId, playerId, latency 추출
const { deviceId, playerId, latency } = payload;
console.log(`Initial connection request - DeviceId: ${deviceId}, PlayerId: ${playerId}`);
// deviceId로 사용자 조회
let user = await findUserByDeviceID(deviceId);
console.log('Initial handler - Existing user:', user); // 기존 사용자 정보 로그 출력
// 사용자가 존재하지 않는 경우 새 사용자 생성
if (!user) {
user = await createUser(deviceId);
console.log('Initial handler - New user created:', user); // 새 사용자 생성 로그 출력
} else {
// 사용자가 존재하는 경우 로그인 정보 업데이트
await updateUserLogin(user.id);
console.log('Initial handler - User login updated'); // 로그인 업데이트 로그 출력
}
// deviceId를 userId로 사용하여 기존 사용자 세션 제거
const existingUser = getUserById(deviceId);
if (existingUser) {
console.log('Initial handler - Removing existing user session'); // 기존 사용자 세션 제거 로그 출력
removeUser(existingUser.socket); // 기존 사용자 세션 제거
}
// 모든 게임 세션 정보 가져오기
const gameSessions = getAllGameSessions();
if (gameSessions.length === 0) {
throw new Error('No active game sessions available'); // 활성 게임 세션이 없으면 오류 발생
}
// 첫 번째 게임 세션 사용 (또는 다른 로직으로 게임 세션 선택)
const gameSession = gameSessions[0];
// 새로운 세션 추가할 때 deviceId를 userId로 사용
const newUser = addUser(deviceId, socket); // 사용자 세션 추가
console.log('Initial handler - User session added:', {
id: newUser.id,
sessionsCount: userSessions.length // 현재 사용자 세션 수 로그 출력
});
gameSession.addUser(newUser); // 게임 세션에 사용자 추가
// 초기 응답 생성
const initialResponse = createResponse(
HANDLER_IDS.INITIAL, // 핸들러 ID
RESPONSE_SUCCESS_CODE, // 성공 응답 코드
{
userId: deviceId, // deviceId를 userId로 사용
playerId: playerId, // 플레이어 ID
latency: latency, // 지연 시간
gameId: gameSession.id // 게임 세션 ID 포함
},
deviceId // 요청을 보낸 사용자 ID
);
// 초기 응답 전송
socket.write(initialResponse);
} catch (error) {
// 오류 발생 시 로그 출력 및 오류 처리
console.error('Error in initialHandler:', error);
handleError(socket, error); // 오류 처리 유틸리티 호출
}
};
// 핸들러 함수 내보내기
export default initialHandler;
더보기
// src/session/user.session.js
/*
이 코드는 사용자 세션을 관리하는 기능을 제공합니다.
사용자 추가, 제거, ID 또는 소켓을 통한 사용자 조회 및 다음 시퀀스 값을 가져오는 기능을 포함합니다.
각 사용자는 'User' 클래스의 인스턴스로 생성되며, 세션 관리를 위해 사용자 배열에 저장됩니다.
*/
import { userSessions } from './sessions.js'; // 사용자 세션 목록을 임포트
import User from '../classes/models/user.class.js'; // User 클래스 임포트
// 사용자 추가 함수
export const addUser = (id, socket) => {
console.log(`Adding user to sessions - ID: ${id}`); // 사용자 추가 로그 출력
// 이미 존재하는 세션인지 확인
const existingSession = userSessions.find(user => user.id === id); // ID로 기존 사용자 세션 조회
if (existingSession) {
console.log(`Removing existing session for user ${id}`); // 기존 세션 제거 로그 출력
removeUser(existingSession.socket); // 기존 세션 제거
}
// 새로운 사용자 인스턴스 생성
const user = new User(id, socket);
userSessions.push(user); // 사용자 배열에 추가
console.log(`User added successfully. Total sessions: ${userSessions.length}`); // 추가 성공 로그 출력
console.log('Current sessions:', userSessions.map(u => u.id)); // 현재 세션 로그 출력
return user; // 추가된 사용자 반환
};
// 사용자 제거 함수
export const removeUser = (socket) => {
// 소켓을 통해 사용자 세션 조회
const index = userSessions.findIndex(user => user.socket === socket);
if (index !== -1) {
const removedUser = userSessions.splice(index, 1)[0]; // 사용자 배열에서 제거
console.log(`Removed user ${removedUser.id} from sessions`); // 제거 성공 로그 출력
return removedUser; // 제거된 사용자 반환
}
console.log('No user found to remove'); // 제거할 사용자 미발견 로그 출력
return null; // 제거할 사용자가 없을 경우 null 반환
};
// ID로 사용자 조회 함수
export const getUserById = (id) => {
console.log(`Looking for user with ID: ${id}`); // 사용자 조회 로그 출력
console.log('Current sessions:', userSessions.map(u => u.id)); // 현재 세션 로그 출력
// ID로 사용자 조회
const user = userSessions.find(user => user.id === id);
console.log("저장된 유저 보기", userSessions); // 현재 저장된 사용자 목록 로그 출력
if (user) {
console.log('User found'); // 사용자 발견 로그 출력
return user; // 발견된 사용자 반환
} else {
console.log('User not found'); // 사용자 미발견 로그 출력
return null; // 사용자가 없을 경우 null 반환
}
};
// 소켓으로 사용자 조회 함수
export const getUserBySocket = (socket) => {
return userSessions.find((user) => user.socket === socket); // 소켓을 통해 사용자 조회
};
// 다음 시퀀스 값 가져오기 함수
export const getNextSequence = (id) => {
const user = getUserById(id); // ID로 사용자 조회
if (user) {
return user.getNextSequence(); // 사용자가 존재할 경우 다음 시퀀스 값 반환
}
return null; // 사용자가 없을 경우 null 반환
};
'TIL' 카테고리의 다른 글
TIL_2025-01-15 (0) | 2025.01.16 |
---|---|
TIL_2025-01-14 (1) | 2025.01.14 |
TIL_2025-01-09 (0) | 2025.01.10 |
TIL_2025-01-08 (0) | 2025.01.08 |
TIL_2025-01-07 (0) | 2025.01.07 |
댓글