25.10.16 개발일지 / C# 명코파크 (4일차)

2025. 11. 13. 17:15·LMS 7/개발일지

이동 및 점프 구현

전에 이동과 점프에 대한 입력은 INPUT으로 모두 받을 수 있었다.

이제 이걸 어떻게 처리를 할지가 문제임

 

1. SERVER

<GameServer.cs>

// 점프를 위한 물리요소
private const float Gravity = 0.6f;
private const float JumpPower = 10f;
private const float GroundY = 0f;
private const int TickRate = 30; // 30ms 마다
// 점프를 위한 루프
// StartAsync() 에서 비동기적으로 계속 실행됨
private async Task PhysicsLoop()
{
    while (true)
    {
        List<KeyValuePair<TcpClient, PlayerState>> snapshot;
        lock (_lockPlayers)
            snapshot = _players.ToList();

        foreach (var kv in snapshot)
        {
            var player = kv.Value;

            // 점프 중이면 Y 속도 적용
            player.y += player.vy;

            // 중력 적용
            player.vy -= Gravity;

            // 바닥 충돌의 경우
            if (player.y < GroundY)
            {
                player.y = GroundY;
                player.vy = 0;
                player.IsJumping = false;
            }
            lock (_lockPlayers)
                _players[kv.Key] = player;
        }

        List<PlayerState> states;
        lock (_lockPlayers)
            states = _players.Values.ToList();

        await BroadcastAsync(MessageType.STATE_SHORT, states);
        await Task.Delay(TickRate);
    }
}
public async Task StartAsync()
{
    _listener.Start();
    Console.WriteLine("Server Start... Port is 5000");

    _ = PhysicsLoop(); // 점프 관련 루프
    _ = TimeoutLoop(); // 접속해제 관련 루프

    while (true)
    {
        var client = await _listener.AcceptTcpClientAsync();
        _lastSeen[client] = DateTime.UtcNow; // 접속 시간 기록
        lock (_lockClients) _clients.Add(client);

        Console.WriteLine($"클라이언트 연결됨({client.Client.RemoteEndPoint})");
        _ = HandleClientAsync(client); // 비동기 수신(await과 다르게 기다리지 않고 무시)
    }
}
private async Task HandleClientAsync(TcpClient client)
{
    var stream = client.GetStream();

    try
    {
        while (true)
        {
            var (header, json) = await Packet.ReceiveAsync(stream);
            _lastSeen[client] = DateTime.UtcNow;

            //Console.WriteLine($"[수신] {header.MsgType} : {json}");

            switch (header.MsgType)
            {
                case MessageType.LOGIN_REQ:
                    await HandleLoginAsync(client, json);
                    break;

                case MessageType.INPUT: // 유저의 키 입력은 INPUT으로 들어오고, 이 case로 처리됨
                    var input = JsonSerializer.Deserialize<KeyState>(json)!;

                    PlayerState player;
                    lock (_lockPlayers) player = _players[client];

                    const float speed = 5f;
                    // 좌 우 이동
                    if (input.Left) player.x -= speed;
                    if (input.Right) player.x += speed;
                    // 점프
                    if (input.Jump && !player.IsJumping)
                    {
                        player.vy = JumpPower;
                        player.IsJumping = true;
                    }

                    lock (_lockPlayers) _players[client] = player;

                    List<PlayerState> states;
                    lock (_lockPlayers) states = _players.Values.ToList();

                    // 이동 및 점프에 관한 정보는 STATE_SHORT로 브로드캐스트함.
                    await BroadcastAsync(MessageType.STATE_SHORT, states);
                    break;
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"클라이언트 종료 : {ex.Message}");

        await DisconnectAsync(client, "exception");
    }
}

 

2. CLIENT

<GameForm.cs>

internal void HandleServerMessage(MessageType type, string json)
{
    switch (type)
    {
        case MessageType.STATE_LONG:
            var player = JsonSerializer.Deserialize<PlayerState>(json);
            if (player != null)
                Invoke(() =>
                {
                    if (player.IsLeaving)
                    {
                        listBox2.Items.Add($"[REMOVE] {player.Nickname} (ID:{player.Id})");
                        RemoveCharacter(player.Id);
                    }
                    else
                    {
                        UpdateOrCreateCharacter(player);
                    }
                });
            break;
        case MessageType.STATE_SHORT: // 이동 및 점프 처리부
            // 모든 클라이언트들의 정보를 업데이트 해야하므로 List 형식으로 받음
            var list = JsonSerializer.Deserialize<List<PlayerState>>(json);
            if (list != null)
            {
                // 디버그용
                listBox2.Items.Add($"[STATE_SHORT] 수신됨 ({list.Count}명)");
                foreach (var p in list)
                    listBox2.Items.Add($"-> {p.Nickname} ({p.x}, {p.y})");
                // 실제분기
                Invoke(() =>
                {
                    foreach (var p in list)
                        UpdateOrCreateCharacter(p);
                });
            }
            break;
    }
}
private void UpdateOrCreateCharacter(PlayerState player)
{
    listBox2.Items.Add($"[UPDATE] {player.Nickname} -> ({player.x}, {player.y})");

    var ground = floors.FirstOrDefault();
    int groundY = ground?.Top ?? this.ClientSize.Height - 40; // 바닥 두께 빼기
    int chaHeight = 50; // 캐릭터 높이

    int renderY = groundY - (int)player.y - chaHeight;
    int renderX = (int)player.x;

    int direction = 0; // 0: 정지, -1: 왼쪽, +1: 오른쪽

    if(prevX.TryGetValue(player.Id, out var prev))
    {
        if (player.x < prev) direction = -1;
        else if(player.x > prev) direction = 1;
    }

    // 현재 좌표 저장
    prevX[player.Id] = player.x;

    // 이미 Id에 맞는 PictureBox가 생성되어 있다면 위치만 변경(이동 및 점프 시)
    if (players.TryGetValue(player.Id, out var pb))
    {
        pb.Location = new Point(renderX, renderY);

        // 방향에 따라 이미지 변경
        if(direction != 0)
        {
            pb.Image = (direction == -1)
                ? PlayerColorsLeft[(player.Id - 1) % PlayerColorsLeft.Length]
                : PlayerColorsRight[(player.Id - 1) % PlayerColorsRight.Length];
        }
    }
    // 아닌 경우에는 PictureBox를 새로 생성한다(LOGIN 시)
    else
    {
        pb = new PictureBox
        {
            Name = player.Nickname,
            Size = new Size(chaHeight, chaHeight),
            SizeMode = PictureBoxSizeMode.StretchImage,
            Image = PlayerColorsRight[(player.Id - 1) % PlayerColorsRight.Length],
            Location = new Point(renderX, renderY),
            BackColor = Color.Transparent
        };

        listBox2.Items.Add($"STATE_LONG: {player.Nickname} ({player.x}, {player.y})");

        Controls.Add(pb);
        pb.BringToFront();
        players[player.Id] = pb;

        prevX[player.Id] = player.x;
    }
}

이미지 추가 및 방향키에 따른 이미지 변경

1. CLIENT

> 먼저 resources(리소스)에 모두 추가함

 

<GameForm.cs>

// Id와 PictureBox 딕셔너리를 통해 본인을 포함한 모든 플레이어 관리
Dictionary<int, PictureBox> players = new Dictionary<int, PictureBox>();

// 이전 프레임 좌표 기억
// 이것과 현재를 비교해 왼쪽, 오른쪽을 구분함
private Dictionary<int, float> prevX = new Dictionary<int, float>();

// 좌우 이미지
private readonly Image[] PlayerColorsLeft = new Image[]
{
    Properties.Resources.RED_LEFT,
    Properties.Resources.ORANGE_LEFT,
    Properties.Resources.YELLOW_LEFT,
    Properties.Resources.GREEN_LEFT,
    Properties.Resources.BLUE_LEFT,
    Properties.Resources.NABY_LEFT,
    Properties.Resources.PURPLE_LEFT,
    Properties.Resources.WHITE_LEFT
};
private readonly Image[] PlayerColorsRight = new Image[]
{
    Properties.Resources.RED,
    Properties.Resources.ORANGE,
    Properties.Resources.YELLOW,
    Properties.Resources.GREEN,
    Properties.Resources.BLUE,
    Properties.Resources.NABY,
    Properties.Resources.PURPLE,
    Properties.Resources.WHITE
};
private void UpdateOrCreateCharacter(PlayerState player)
{
    listBox2.Items.Add($"[UPDATE] {player.Nickname} -> ({player.x}, {player.y})");

    var ground = floors.FirstOrDefault();
    int groundY = ground?.Top ?? this.ClientSize.Height - 40; // 바닥 두께 빼기
    int chaHeight = 50; // 캐릭터 높이

    int renderY = groundY - (int)player.y - chaHeight;
    int renderX = (int)player.x;

    // 방향에 대한 변수
    int direction = 0; // 0: 정지, -1: 왼쪽, +1: 오른쪽
    // prevX의 키와 값을 가지고, 현재(player.x)와 비교해 변수를 변경
    if(prevX.TryGetValue(player.Id, out var prev))
    {
        if (player.x < prev) direction = -1;
        else if(player.x > prev) direction = 1;
    }
    // 현재 x좌표 저장
    prevX[player.Id] = player.x;

    if (players.TryGetValue(player.Id, out var pb))
    {
        // Id(Key)에 맞는 pb(Value)의 위치를 변경하고,
        pb.Location = new Point(renderX, renderY);

        // 방향 변수에 따라 이미지 변경
        if(direction != 0)
        {
            pb.Image = (direction == -1)
                // player.Id는 1부터 시작하므로 0부터 7까지 모두 사용하고, 8부터 다시 0부터 시작 가능함
                ? PlayerColorsLeft[(player.Id - 1) % PlayerColorsLeft.Length]
                : PlayerColorsRight[(player.Id - 1) % PlayerColorsRight.Length];
        }
    }
    else
    {
        pb = new PictureBox
        {
            Name = player.Nickname,
            Size = new Size(chaHeight, chaHeight),
            SizeMode = PictureBoxSizeMode.StretchImage,
            Image = PlayerColorsRight[(player.Id - 1) % PlayerColorsRight.Length],
            Location = new Point(renderX, renderY),
            BackColor = Color.Transparent
        };

        listBox2.Items.Add($"STATE_LONG: {player.Nickname} ({player.x}, {player.y})");

        Controls.Add(pb);
        pb.BringToFront();
        players[player.Id] = pb;
    }
}

바닥을 만들고, 캐릭터들을 그 위에 고정

1. CLIENT

<GameForm.cs>

// 맵 - 바닥
private List<PictureBox> floors = new List<PictureBox>();
private PictureBox? mainFloor;
private void InitFloor()
{
    // 메인 바닥 생성
    mainFloor = new PictureBox
    {
        Name = "Floor_Main",
        BackColor = Color.SaddleBrown,
        Height = 40,
        Width = this.ClientSize.Width,
        Top = this.ClientSize.Height - 40,
        Left = 0
    };

    // 폼 크기가 바뀌면 자동으로 폭 조정하고 컨트롤에 추가
    mainFloor.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom;
    this.Controls.Add(mainFloor);

    // PictureBox List인 floors에도 추가시킴 
    floors.Add(mainFloor);
}
private void UpdateOrCreateCharacter(PlayerState player)
{
    listBox2.Items.Add($"[UPDATE] {player.Nickname} -> ({player.x}, {player.y})");

    // 바닥(ground)은 가장 첫번째 것을 사용
    var ground = floors.FirstOrDefault();
    // groundY 는 바닥의 위치
    // ground가 null이 아니라면 ground.Top을 사용
    // ground.Top이 null이 아니라면 그대로 사용하고, null이라면 ClientSize.Height - 40 값을 사용
    int groundY = ground?.Top ?? this.ClientSize.Height - 40;
    int chaHeight = 50; // 캐릭터 높이

    // render는 서버에서는 y 좌표가 커질수록 위로 올라가는 반면
    // 클라이언트는 아래로 내려가기 때문에 이를 반전해주기 위한 좌표계산임
    // x는 같고, y만 땅 높이에서 플레이어의 서버 좌표 및 높이를 빼서 사용함
    int renderY = groundY - (int)player.y - chaHeight;
    int renderX = (int)player.x;

    int direction = 0; // 0: 정지, -1: 왼쪽, +1: 오른쪽

    if(prevX.TryGetValue(player.Id, out var prev))
    {
        if (player.x < prev) direction = -1;
        else if(player.x > prev) direction = 1;
    }

    // 현재 좌표 저장
    prevX[player.Id] = player.x;

    if (players.TryGetValue(player.Id, out var pb))
    {
        pb.Location = new Point(renderX, renderY);

        // 방향에 따라 이미지 변경
        if(direction != 0)
        {
            pb.Image = (direction == -1)
                ? PlayerColorsLeft[(player.Id - 1) % PlayerColorsLeft.Length]
                : PlayerColorsRight[(player.Id - 1) % PlayerColorsRight.Length];
        }
    }
    else
    {
        pb = new PictureBox
        {
            Name = player.Nickname,
            Size = new Size(chaHeight, chaHeight),
            SizeMode = PictureBoxSizeMode.StretchImage,
            Image = PlayerColorsRight[(player.Id - 1) % PlayerColorsRight.Length],
            Location = new Point(renderX, renderY),
            BackColor = Color.Transparent
        };

        listBox2.Items.Add($"STATE_LONG: {player.Nickname} ({player.x}, {player.y})");

        Controls.Add(pb);
        pb.BringToFront();
        players[player.Id] = pb;

    }
}

실행영상

 

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

'LMS 7 > 개발일지' 카테고리의 다른 글

25.10.20 개발일지 / C# 네트워크  (0) 2025.11.13
25.10.17 개발일지 / C# 명코파크 (5일차)  (0) 2025.11.13
25.10.15 개발일지 / C# 명코파크 (3일차)  (0) 2025.11.13
25.10.14 개발일지 / C# 명코파크 (2일차)  (0) 2025.11.13
25.10.13 개발일지 / C# 명코파크 (1일차)  (0) 2025.11.13
'LMS 7/개발일지' 카테고리의 다른 글
  • 25.10.20 개발일지 / C# 네트워크
  • 25.10.17 개발일지 / C# 명코파크 (5일차)
  • 25.10.15 개발일지 / C# 명코파크 (3일차)
  • 25.10.14 개발일지 / C# 명코파크 (2일차)
m_Dev
m_Dev
  • m_Dev
    m_Dev
    m_Dev
  • 전체
    오늘
    어제
    • 분류 전체보기
      • MAIN STUDY
        • 정보보안
        • 빅데이터
        • 정보처리
        • 컴퓨터 구조
        • 기타
      • JOB
        • Study
        • Project
      • LMS 7
        • 개발일지
      • FRAMEWORK
        • Qt
        • MFC
        • Winform
        • WPF
        • MAUI
      • NETWORK
        • Study
        • Assignment
      • PYTHON
        • Set
        • Study
        • Assignment
        • Project
      • C
        • Set
        • Study
        • Assignment
        • Project
      • C++
        • Set
        • Study
        • Assignment
        • Project
      • C#
        • Set
        • Study
        • Assignment
        • Project
      • DATABASE
        • MySQL
        • SQLite
      • IDE
        • VisualStudioCode
        • VisualStudio
        • Pycharm
        • Colab
      • 기타
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
m_Dev
25.10.16 개발일지 / C# 명코파크 (4일차)
상단으로

티스토리툴바