고갱

[학교 과제] 3D 게임 개발 #2, 게임 로직과 멀티 플레이 1차 구현 본문

게임 개발/Unity

[학교 과제] 3D 게임 개발 #2, 게임 로직과 멀티 플레이 1차 구현

주인장 고갱 2025. 5. 27. 05:20

그래서 주말에는 기초적인 게임 로직과 멀티 플레이의 기본이 되는 Packet 구현 과정을 거쳤다.

외부 라이브러리를 사용하면 훨씬 높은 퀄리티를 손쉽게 만들 수 있겠지만, 아무래도 학교 과제인 만큼 외부 라이브러리를 최대한 사용하지 않고 직접 구현하는 것을 목표로 하였다.

 

🤔 그래서 무엇을 했을까?

1. 게임 로직 구현

게임 화면

 

위에서 간단히 언급하기는 했지만 첫 번째로는 게임 로직이다.

이제 캐릭터가 밟은 땅은 1초 후에 무너지게 구현하였다.

 

구현할 때 고민을 많이했다.

가장 먼저 생각했던 것은 캐릭터를 기준으로 직선으로 아래로 Ray를 쏴서 부딪히는 땅을 무너지게 하는 것.

하지만 큰 문제가 있었다.

직선 형태로 쏘면 결국 걸쳐서 서있는 경우에는 땅이 무너지지 않게 되기 때문이다.

 

두 번째로는 캐릭터 하위에 IsTrigger를 켜둔 Collider를 둬서 충돌하는 땅을 인식하는 것이였다.

이 방법은 그래도 잘 작동할 것이지만, 그래도 최대한 하이라키를 간소하게 두고 싶은 마음이여서 보류하였다.

 

그래서 결국 선택한 것은 원형 모양으로 Ray를 쏘는 것이였다.

코드

 

코드는 위와 같이 구현 해두었는데, 캐릭터의 너비가 0.15 이므로 0.15 너비를 가진 원형 형태로 충돌체를 감지하였다.

 

무너지는 함수

 

무너지는 함수도 크게 어려운 부분이 없다

tag를 통해서 이미 무너지고 있는 땅인지를 검사하게 해주었고, 만약 이미 무너지고 있는 땅이라면 처리하지 않게 하였다.

 

이후엔 Sin함수와 Cos함수를 이용하여 0.05 진폭을 가진 50Hz의 움직임을 구현해주었다.

 

이게 왜 가능하냐면,

Time.time 은 게임 플레이가 지속됨에 따라 점차 쌓여가는 값이고 (시간)

Time.deltaTime은 이전 프레임과 현재 프레임 사이의 시간 값이기 때문이다.

 

즉, Time.time 을 이용한다면 50Hz 주기를 가진 일정한 간격의 흔들림을 생성할 수 있다!

 

그리고 1초동안 충분히 흔들리게 하였다면 비로소 게임 오브젝트를 비활성화 해줌으로써 실행이 끝난다.

물론 Destory 해주는 것이 성능 상 이점이 있긴 하겠지만, 무너진 땅이 일정 시간 후에 다시 생성되는 로직도 생각 중이라서 일단 비활성화로 해주었다.

(애초에 성능 따질거였으면 모두 GPU에 그리게 하고 물리만 Unity에서 처리시켰겠지)

 

 

2. 멀티플레이 기본 로직

 

이건 좀 에매하다.

완전히 구현했다기보다는 패킷을 구현하였을 뿐이다.

[Serializable]
public abstract class BasePacket
{

    public enum PacketType : byte
    {
        #region 로비
        JOIN, //방 접속
        JOIN_ACK,

        READY, //레디
        READY_ACK,

        START, //게임 시작
        START_ACK,

        KICK, //게임 추방
        KICK_ACK,
        #endregion

        #region 움직임 관련
        MOVE, //움직이기
        MOVE_ACK,

        CROUCHING, //엎드리기
        CROUCHING_ACK,

        JUMP, //점프
        JUMP_ACK,
        #endregion

        #region 공통
        LEAVE,
        LEAVE_ACK,

        PLAYER_LIST,
        #endregion
    }

    public abstract PacketType packetType { get; }
    public byte clientId;

    public BasePacket(byte clientId)
    {
        this.clientId = clientId;
    }

    public abstract void ReadFrom(BinaryReader reader);

    //virtual로 선언
    public virtual void WriteTo(BinaryWriter writer)
    {
        writer.Write((byte)packetType);
        writer.Write(clientId);
    }

    public byte[] ToBytes()
    {
        using (MemoryStream ms = new MemoryStream())
        using (BinaryWriter writer = new BinaryWriter(ms))
        {
            WriteTo(writer); // 하위 클래스의 오버라이드 호출
            return ms.ToArray();
        }
    }

    public static BasePacket FromBytes(byte[] data)
    {
        using (MemoryStream ms = new MemoryStream(data))
        using (BinaryReader reader = new BinaryReader(ms))
        {
            PacketType type = (PacketType)reader.ReadByte();
            byte clientId = reader.ReadByte();

            BasePacket packet = type switch
            {
                PacketType.JOIN => new JoinPacket(),
                PacketType.JOIN_ACK => new JoinPacketAck(clientId),
                PacketType.PLAYER_LIST => new PlayerListPacket(),
                // 다른 타입 추가
                _ => throw new Exception("Unknown packet type")
            };

            packet.clientId = clientId;
            packet.ReadFrom(reader);
            return packet;
        }
    }
}

 

예전에 RTS 게임 기획할 때 기초적으로 생각해둔 패킷 구조를 거의 유사하게 구현했다.

BasePacket을 abstract로 선언함으로써 모든 패킷의 기초 뼈대를 만들어두었고,

이를 상속하여 상세적으로 패킷 하나하나를 구성시키게 하였다.

 

[Serializable]
public class ReadyPacket : BasePacket
{
    public override PacketType packetType => PacketType.READY;

    public ReadyPacket(byte clientId) : base(clientId) { }

    public override void ReadFrom(BinaryReader reader)
    {
        //준비 패킷은 할 게 없음
    }
}

 

그래서 하위 패킷을 보면 상당히 구조가 간편하게 구성되어 있음을 볼 수 있다!

만약 하위 패킷에 고유한 필드가 존재하면 ReadFrom 함수와 WriteTo 함수를 오버라이딩하여 해당 필드도 직렬화해줄 수 있다.

이 얼마나 관리하기 쉬운 패킷 구조인가..

 

그리고 그 다음에 해야할 일은 패킷을 수신했을 때 어떻게 처리하는 지가 관건이다.

사실 나는 예전에 Nukkit 이라는 프로젝트를 분석했던 경험이 있어서 해당 프로젝트의 작동 방식과 유사하게 (동일하진 않지만) 구현하기로 하였다.

 

GitHub - Nukkit/Nukkit: Nukkit is a Nuclear-Powered Server Software For Minecraft: Pocket Edition

Nukkit is a Nuclear-Powered Server Software For Minecraft: Pocket Edition - Nukkit/Nukkit

github.com

(게임 서버 소프트웨어, 여기서는 패킷을 수신하면 이벤트 핸들러 어노테이션을 통해 처리해주었다)

 

 

그래서 도입한 방법

 

그래서 나도 PacketHandler이라는 어노테이션을 만들어주어 특정 패킷을 수신할 수 있도록 하는 기능을 구현하였다.

 

물론 Reflection을 사용하는 만큼 성능이 저하되는 것은 인지하고 있지만, 뭐 이게 엄청나게 심각한 저하도 아니고 유지보수성을 위해서는 오히려 좋은 선택이라고 생각한다.

 

각각 서버와 클라이언트 접속 화면

 

그리하여 좌 상단에 플레이어 목록이 표시되는것과 같이 드디어 로비 접속까지 구현하는 것에 성공하였다.

(각각 이름이 '서버', '클라이언트' 이다.)

 

 

😵 다음 계획

이제 다음은 실제 움직임 동기화라던가 다른 구체적인 부분에 대한 네트워크 구현을 할 차례이다.

그 후에는 게임 시작, 게임 오버 등 게임 로직을 더 세부적으로 짜야할 것이고..

아무튼 아직은 갈 길이 먼 것 같다.

 

 

 

👍 참고 자료

Nukkit 프로젝트

 

GitHub - Nukkit/Nukkit: Nukkit is a Nuclear-Powered Server Software For Minecraft: Pocket Edition

Nukkit is a Nuclear-Powered Server Software For Minecraft: Pocket Edition - Nukkit/Nukkit

github.com