본문 바로가기

- Programming/- C#

★ 12. c# 네트워크 개발 p11

반응형

==================================================================

모든 출처는

- 유니티 개발자를 위한 C#으로 온라인 게임 서버 만들기 - 저자 이석현, 출판사 한빛미디어

그리고 URL :

http://lab.gamecodi.com/board/zboard.php?id=GAMECODILAB_Lecture_series&page=2&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=hit&desc=asc&no=80

==================================================================


C#으로 게임 서버 만들기 - 9. 세균전 - 클라이언트 구현


지난 시간에는 게임방 입장 패킷을 요청하는 부분까지 작성해봤습니다.

이번 시간에는 본격적으로 클라이언트 쪽의 게임 로직을 구현해보도록 하겠습니다.

마치 새하얀 도화지에 게임의 모습을 색칠해 나가는 시간이될 것 같습니다.


게임 방 입장

클라이언트에서 서버로 게임 방 입장 패킷을 보내면 서버는 두 명의 클라이언트를 모은 뒤

START_LOADING 패킷을 각각의 클라이언트에게 전달합니다.

이 패킷을 받은 클라이언트에서는 게임 방에 입장하도록 처리해줍니다.


두 명의 클라이언트로부터 ENTER_GAME_ROOM_REQ 패킷을 받으면 서버는 START_LOADING 패킷을 모든 클라이언트들에게 전달해준다.



클라이언트는 게임방 입장 패킷을 요청한 후 상대방을 기다린다.

1
2
3
this.battle_room.gameObject.SetActive(true);
this.battle_room.start_loading(player_index);
gameObject.SetActive(false);
cs

클라이언트의 게임방 입장 코드

서버로부터 START_LOADING 패킷을 전달 받은 뒤 게임방이 구현된 오브젝트를 활성화 시키고
start_loading 메소드를 호출하여 로딩을 시작하는 코드입니다.
현재 오브젝트인 MainTitle 오브젝트는 사용할 필요가 없으므로 SetActive(false)를 통해 비활성화 시켜서
코드가 수행되지 않도록 처리합니다.


상대방이 게임 방 입장 요청을 하게 되면 서버로부터 START_LOADINg 패킷을 전달 받아 게임방에 입장하게 됩니다.

리소스 로딩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using FreeNet;
using VirusWarGameServer;
 
public class CBattleRoom : MonoBehaviour {
 
    enum GAME_STATE
    {
        READY = 0,
        STARTED
    }
 
    // 가로, 세로 칸 수를 의미한다.
    public static readonly int COL_COUNT = 8;
 
    List<CPlayer> players;
 
    // 진행중인 게임 보드 판의 상태를 나타내는 데이터.
    List<short> board;
 
    // 0~49 까지의 인덱스를 갖고 있는 보드 판 데이터
    List<short> table_board;
 
    // 공격 가능한 범위를 나타낼 때 사용하는 리스트
    List<short> available_attack_cells;
 
    // 현재 턴을 진행중인 플레이어 인덱스
    byte current_player_index;
 
    // 서버에서 지정해준 본인의 플레이어 인덱스
    byte player_me_index;
 
    // 상황에 따른 터치 입력을 처리하기 위한 변수.
    byte step;
 
    // 게임 종료 후 메인으로 돌아갈 때 사용하기 위한 MainTitle 객체의 레퍼런스.
    CMainTitle main_title;
 
    // 네트워크 데이터 송, 수신을 위한 네트워크 매니저 레퍼런스.
    CNetworkManager network_manager;
 
    // 게임 상태에 따라 각각 다른 GUI 모습을 구현하기 위해 필요한 상태 변수.
    GAME_STATE game_state;
 
    // OnGUI 메소드에서 호출할 델리게이트.
    // 여러 종류의 메소드를 만들어 놓고 상황에 맞게 draw에 대입해주는 방식으로 GUI를 변경시킨다.
    delegate void GUIFUNC();
    GUIFUNC draw;
 
    // 승리한 플레이어 인덱스.
    // 무승부일때는 byte.MaxValue가 들어간다.
    byte win_player_index;
 
    // 점수를 표시하기 위한 이미지 숫자 객체.
    // 선명하고 예쁘게 표현하기 위해 폰트 대신 이미지로 만들어 사용한다.
    CImageNumber score_images;
 
    // 현재 진행중인 플레이어를 나타내는 객체
    CBattleInfoPanel battle_info;
 
    // 게임이 종료되었는지를 나타내는 플래그.
    bool is_game_finished;
 
    // 각종 이미지 텍스쳐들.
    List<Texture> img_players;
    Texture background;
    Texture blank_image;
    Texture game_board;
 
    Texture graycell;
    Texture focus_cell;
 
    Texture win_img;
    Texture lose_img;
    Texture draw_img;
    Texture gray_transparent;
 
    void Awake()
    {
        this.table_board = new List<short>();
        this.available_attack_cells = new List<short>();
 
        this.graycell = Resources.Load("images/graycell"as Texture;
        this.focus_cell = Resources.Load("images/border"as Texture;
 
        this.blank_image = Resources.Load("images/blank"as Texture;
        this.game_board = Resources.Load("images/gameboard"as Texture;
        this.background = Resources.Load("images/gameboard_bg"as Texture;
        this.img_players = new List<Texture>();
        this.img_players.Add(Resources.Load("images/red"as Texture);
        this.img_players.Add(Resources.Load("images/blue"as Texture);
 
        this.win_img = Resources.Load("images/win"as Texture;
        this.lose_img = Resources.Load("images/lose"as Texture;
        this.draw_img = Resources.Load("images/draw"as Texture;
        this.gray_transparent = Resources.Load("images/gray_transparent"as Texture;
 
        this.board = new List<short>();
 
        this.network_manager = GameObject.Find("NetworkManager").GetComponent<CNetworkManager>();
 
        this.game_state = GAME_STATE.READY;
 
        this.main_title = GameObject.Find("MainTitle").GetComponent<CMainTitle>();
        this.score_images = gameObject.AddComponent<CImageNumber>();
 
        this.win_player_index = byte.MaxValue;
        this.draw = this.on_gui_playing;
        this.battle_info = gameObject.AddComponent<CBattleInfoPanel>();
    }
cs

Awake 메소드에서 게임에 필요한 리소스들을 로딩합니다.
리소스 로딩은 게임 시작 이후 한번만 수행하면 되기 때문에 Awake 메소드에서 처리한 것입니다.
이 메소드는 해당 스크립트가 최초로 활성화 될 때 단 한번만 호출되기 때문입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void reset()
{
    // 보드판 데이터를 모두 초기화 한다.
    this.board.Clear();
    this.table_board.Clear();
    for (int i = 0; i < COL_COUNT * COL_COUNT; ++i)
    {
        this.board.Add(short.MaxValue);
        this.table_board.Add((short)i);
    }
 
    // 보드판에 각 플레이어들의 위치를 입력한다.
    this.players.ForEach(obj =>
    {
        obj.cell_indexes.ForEach(cell =>
        {
            this.board[cell] = obj.player_index;
        });
    });
}
 
void clear()
{
    this.current_player_index = 0;
    this.step = 0;
    this.draw = this.on_gui_playing;
    this.is_game_finished = false;
}
 
/// <summary>
/// 게임방에 입장할 때 호출된다. 변수 초기화 등 게임 플레이를 위한 준비 작업을 진행한다.
/// </summary>
/// <param name="player_me_index"></param>
public void start_loading(byte player_me_index)
{
    clear();
 
    this.network_manager.message_receiver = this;
    this.player_me_index = player_me_index;
 
    CPacket msg = CPacket.create((short)PROTOCOL.LOADING_COMPLETED);
    this.network_manager.send(msg);
}
cs

clear 메소드는 클라이언트에서 유저의 입력을 받는데 필요한 데이터들을 초기화하는 역할을 담당합니다.
본격적인 게임 진행이 이루어지기 이전에 유저의 불필요한 입력을 방지하고 방에 입장한 직후 표시될
GUI를 구성하는 코드가 들어 있습니다.

start_loading 메소드는 게임 방에 입장한 뒤 바로 호출됩니다.
이 메소드에서는 서버에서 지정해준 플레이어 인덱스를 보관해놓고 LOADING_COMPLETED 패킷을 서버로 보냅니다.
LOADING_COMPLETED 패킷을 보내면 서버에서는 해당 클라이언트가 정상적으로 게임 방에 입장을 하였다는 뜻입니다.
만약 로딩 중간에 네트워크가 끊어지는 등 오류가 발생하여 LOADING_COMPLETED 패킷을 서버로 전달하지 못한 경우에는
서버에서 해당 클라이언트의 로딩 완료 처리를 수행하지 않으며 아직 게임을 진행하지 않도록 구현되어 있습니다.
클라이언트에서는 이런 상황들을 직접 판단하지 않고 서버에 요청한 뒤 응답이 오는 것을 기다렸다가 처리하면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// 클라이언트에서 로딩을 완료한 후 요청함.
/// 이 요청이 들어오면 게임을 시작해도 좋다는 뜻이다.
/// </summary>
/// <param name="sender">요청한 유저</param>
public void loading_complete(CPlayer player)
{
    // 해당 플레이어를 로딩 완료 상태로 변경한다.
    change_playerstate(player, PLAYER_STATE.LOADING_COMPLETE);
 
    // 모든 유저가 준비 상태인지 체크한다.
    if (!allplayers_ready(PLAYER_STATE.LOADING_COMPLETE))
    {
        // 아직 준비가 안된 유저가 있다면 대기한다.
        return;
    }
 
    // 모두 준비 되었다면 게임을 시작한다.
    battle_start();
}
cs

LOADING_COMPLETED 패킷을 처리하는 서버측 코드

서버는 클라이언트에서 보내온 LOADING_COMPLETED 패킷을 수신하여 CGameRoom 클래스의 loading_complete 메소드를 호출합니다.
이 메소드에서는 두 명의 유저가 모두 LOADING_COMPLETED 패킷을 전송하였는지 체크한 뒤 게임을 시작하게 됩니다.
아직 LOADING_COMPLETED 패킷을 보내오지 않은 유저가 남아 있다면 아무 일도 하지 않고 대기합니다.

게임 시작
클라이언트의 로딩 완료 요청 이후 서버에서는 GAME_START 패킷으로 게임의 시작을 알려줍니다.
on_game_start 메소드에서 게임 시작을 위한 준비 작업을 수행합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void on_game_start(CPacket msg)
{
    this.players = new List<CPlayer>();
 
    byte count = msg.pop_byte();
    for (byte i = 0; i < count; ++i)
    {
        byte player_index = msg.pop_byte();
 
        GameObject obj = new GameObject(string.Format("player{0}", i));
        CPlayer player = obj.AddComponent<CPlayer>();
        player.initialize(player_index);
        player.clear();
 
        byte virus_count = msg.pop_byte();
        for (byte index = 0; index < virus_count; ++index)
        {
            short position = msg.pop_int16();
            player.add(position);
        }
 
        this.players.Add(player);
    }
 
    this.current_player_index = msg.pop_byte();
    reset();
 
    this.game_state = GAME_STATE.STARTED;
}
cs

서버에서 보내온 정보를 토대로 플레이어들을 생성하고 초기 위치들을 설정합니다.
첫 턴을 진행할 플레이어 인덱스도 서버에서 받아온 값으로 설정해놓습니다.
모든 정보들의 설정이 완료 되었다면 게임 상태를 GAME_STATE.STARTED로 변경하여 게임을 시작합니다.

이처럼 게임 진행에 필요한 핵심 정보들은 모두 서버에서 받아서 처리해야 합니다.
즉 클라이언트에 보관되어 있는 변수들의 값은 단순히 서버에서 보내온 값을 복사하는 수준이며
크래킹 등을 통해 이 값을 변경한다고 하더라도 서버에는 절대로 반영되지 않기 때문에
상대방 클라이언트를 속이는 행위는 허용될 수 없는 것입니다.
또한 클라이언트에서 서버로 요청을 보내는 경우에도 게임 상태를 변경하라는 명령이 되면 안됩니다.
단순히 무엇을 해달라고 허락을 구하는 모습이 되어야 합니다. 서버에서는 해당 요청이 현재 게임 상황에 비추어 볼 때
정상적인지 판단한 뒤 그때서야 게임 로직에 변경을 가하게 되는 것입니다.
만약 이 룰을 벗어나는 로직이 있을 경우에는 클라이엉ㄴ트가 마음 먹은대로 게임을 바꿀 수 있는 길을
열어주는 꼴이 되기 때문에 요청 패킷 하나 하나를 작성할 때 반드시 주의해야 합니다.

유저의 입력 처리
게임이 시작되었다면 첫 번째 플레이어부터 턴을 시작합니다.
먼저 자신의 세균을 선택하고 이동할 곳을 선택합니다. 그 뒤 게임 룰에 따라 상대방의 세균이 주위에 있으면
감염시켜 자신의 세균으로 만듭니다.
다음 코드는 유저가 보드 판을 터치하였을 때 호출되는 on_click 메소드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
void on_click(short cell)
{
    // 자신의 차례가 아니면 처리하지 않고 리턴
    if (this.player_me_index != this.current_player_index)
    {
        return;
    }
 
    //Debug.Log(cell);
 
    switch (this.step)
    {
        case 0:
            if (validate_begin_cell(cell))
            {
                this.selected_cell = cell;
                Debug.Log("go to step2");
                this.step = 1;
 
                refresh_available_cells(this.selected_cell);
            }
            break;
        case 1:
            {
                // 자신의 세균을 터치하였을 경우에는 다시 공격 범위를 계산하여 출력해 준다.
                if (this.players[this.current_player_index].cell_indexes.Exists(obj => obj == cell))
                {
                    this.selected_cell = cell;
                    refresh_available_cells(this.selected_cell);
                    break;
                }
 
                // 게임 룰에 따라서 다른 플레이어의 세균은 선택할 수 없도록 처리한다.
                foreach (CPlayer player in this.players)
                {
                    if (player.cell_indexes.Exists(obj => obj == cell))
                    {
                        return;
                    }
                }
 
                if (CHelper.get_distance(this.selected_cell, cell) > 2)
                {
                    // 2칸을 초과하는 거리는 이동할 수 없다.
                    return;
                }
                
                // 모든 검사가 이므로 서버에 이동 요청을 보낸다.
                CPacket msg = CPacket.create((short)PROTOCOL.MOVING_REQ);
                msg.push(this.selected_cell);
                msg.push(cell);
                this.network_manager.send(msg);
 
                this.step = 2;
            }
            break;
    }
}
cs

유저의 입력은 두 단계로 나뉩니다. 자신의 세균을 선택하는 단계를 0번, 이동할 곳을 선택하는 단계를 1번이라고 하겠습니다.
먼저 0번 단계에서는 선택한 곳이 유효한 곳인지 확인하는 과정을 거칩니다.
서버에서도 같은 처리가 이루어지지만 클라이언트에서 미리 체크한다면 불필요한 오류 상황을 막을 수 있기에
여기서도 검증 루틴을 포함시켰습니다.
정상이라고 판단되면 step = 1로 설정하여 다음 번에 on_click 메소드가 호출되었을 때 1번 단계로 넘어갈 수 있도록 해줍니다.
1번 단계에서는 이동할 곳을 선택했을 때의 처리가 이루어집니다.
여기서도 역시 검증 루틴을 거친 뒤 정상이라고 판단되면 그때서야 서버로 MOVING_REQ 패킷을 전송합니다.
이 때 시작 위치와 이동할 위치를 패킷에 넣어서 서버로 요청하게 됩니다.
요청한 이후에는 step = 2로 설정하여 또 다시 터치하더라도 아무 처리가 이루어지지 않도록 합니다.

서버에서 MOVING_REQ 패킷을 받은 뒤에 모든 데이터가 정상적이라면 클라이언트로 PLAYER_MOVED 패킷을 전달합니다.
PLAYER_MOVED 패킷을 전달하는 서버측 코드를 살펴보겠습니다.

1
2
3
4
5
6
// 최종 결과를 broadcast한다.
CPacket msg = CPacket.create((short)PROTOCOL.PLAYER_MOVED);
msg.push(sender.player_index);      // 누가
msg.push(begin_pos);                // 어디서
msg.push(target_pos);               // 어디로 이동 했는지
broadcast(msg);
cs

이동하게 되는 플레이어의 인덱스와 시작 위치, 이동한 위치를 모든 클라이언트에게 보내줍니다.
앞서 클라이언트에서 보낸 정보와 크게 다를 것 없어 보이지만 서버의 검증을 거친 데이터라는 점에서 큰 차이가 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void on_player_moved(CPacket msg)
{
    byte player_index = msg.pop_byte();
    short from = msg.pop_int16();
    short to = msg.pop_int16();
 
    StartCoroutine(on_selected_cell_to_attack(player_index, from, to));
}
 
IEnumerator on_selected_cell_to_attack(byte player_index, short from, short to)
{
    byte distance = CHelper.howfar_from_clicked_cell(from, to);
    if (distance == 1)
    {
        // 이동 거리가 한 칸이라면 자기 자신을 복제한다.
        yield return StartCoroutine(reproduce(to));
    }
    else if (distance == 2)
    {
        // 이동 거리가 두 칸이라면 해당 위치로 ㅇ동한다.
        this.board[from] = short.MaxValue;
        this.players[player_index].remove(from);
        yield return StartCoroutine(reproduce(to));
    }
 
    // 이동 처리가 다 끝나면 턴을 종료해달라는 패킷을 보낸다.
    CPacket msg = CPacket.create((short)PROTOCOL.TURN_FINISHED_REQ);
    this.network_manager.send(msg);
 
    yield return 0;
}
cs

서버에서 받은 데이터로 세균의 이동을 시작합니다.
on_selected_cell_to_attack 메소드를 호출하여 세균이 이동하는 모습을 구현합니다.
게임 룰에 따라 이동한 거리가 한 칸이라면 자기 자신을 복제하고, 두 칸이라면 이동을 합니다.
세균의 복제 처리를 구현해 놓은 reproduce 메소드는 코루틴으로 되어 있으므로 StartCoroutine 명령어를 사용하여 호출해 줍니다.
이 메소드를 코루틴으로 구성한 이유는 세균들의 이동 모습을 구현하는데 아주 편리한 코딩 수간을 제공해주기 때문입니다.
다음은 reproduce 메소드의 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
IEnumerator reproduce(short cell)
{
    CPlayer current_player = this.players[this.current_player_index];
    CPlayer other_player = this.players.Find(obj.player_index != this.current_player_index);
 
    clear_available_attacking_cells();
 
    // cell을 현재 플레이어의 위치에 추가한다.
    this.board[cell] = current_player.player_index;
    current_player.add(cell);
 
    // 0.5초 대기
    yield return new WaitForSeconds(0.5f);
 
    // 주위에 상대방의 세균이 있다면 감염시킨다.
    List<short> neighbors = CHelper.find_neighbor_cells(cell, other_player.cell_indexes, 1);
    foreach (short obj in neighbors)
    {
        this.board[obj] = current_player.player_index;
        current_player.add(obj);
 
        other_player.remove(obj);
 
        // 하나의 세균을 감염시키고 0.2초 대기한 뒤 다시 세균의 감염을 처리한다.
        yield return new WaitForSeconds(0.2f);
    }
}
 
cs

yield return new WaitForSeconds() 라는 코드가 보이는데
이 코드는 현재 위치에서 몇 초까지 대기한 후 다음 루틴을 수행하라는 의미입니다.
이동하고 상대방 세균을 잡아 먹는 모습은 이처럼 딜레이를 두어 차례대로 진행되도록 구현되어 있습니다.
만약 코루틴을 사용하지 않고 이와 같이 시나리오를 구현하려면 별도의 타이머 시스템을 구축해야 할 것입니다.
이런 기법은 실제 상용 게임을 개발할 때도 유용하게 사용되므로 꼭 기억해 두시기 바랍니다.

클라이언트에서 세균의 이동 처리가 완료되면 턴을 끝내달라는 요청을 서버로 전송합니다.

1
2
CPacket msg = CPacket.create((short)PROTOCOL.TURN_FINISHED_REQ);
this.network_manager.send(msg);
cs

TURN_FINISHED_REQ 패킷을 서버로 전달하면 서버에서는 다음 플레이어로 턴을 넘긴 뒤
클라이언트에서 START_PLAYER_TURN 패킷을 전달합니다.

1
2
3
4
5
6
7
8
9
10
11
12
void on_start_player_turn(CPacket msg)
{
    phase_end();
 
    this.current_player_index = msg.pop_byte();
}
 
void phase_end()
{
    this.step = 0;
    this.available_attack_cells.Clear();
}
cs

서버로부터 START_PLAYER_TURN 패킷을 받은 뒤 처리되는 메소드입니다.
먼저 phase_end 메소드를 호출하여 턴 진행을 위한 변수들을 초기화 한 뒤,
current_player_index의 값을 서버로부터 받은 정보로 갱신합니다.
만약 current_player_index의 값이 본인의 플레이어 인덱스와 같다면 자신의 차례인 것이고
다르다면 상대방의 차례인 것이므로 이 값에 따라서 게임 진행을 제어하도록 처리합니다.

게임 결과 화면 그리기
이런 순서로 승부가 날 때까지 게임을 계속 진행하게 됩니다. 게임 종료 여부를 판단하는 부분은 서버에서 처리합니다.
게임이 종료되면 서버로부터 GAME_OVER 패킷을 전달 받게되며 승리한 플레이어의 정보도 같이 담겨 있습니다.
이 내용을 토대로 게임 결과 화면을 출력해 보겠습니다.

1
2
3
4
5
6
void on_game_over(CPacket msg)
{
    this.is_game_finished = true;
    this.win_player_index = msg.pop_byte();
    this.draw = this.on_gui_game_result;
}
cs

서버로부터 승리한 플레이어의 인덱스를 받아 this.win_player_index 변수에 보관합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/// <summary>
/// 결과 화면 그리기
/// </summary>
void on_gui_game_result()
{
    on_gui_playing();
 
    GUI.DrawTexture(new Rect(00, Screen.width, Screen.height), this.gray_transparent);
    GUI.BeginGroup(new Rect(Screen.width / 2 - 173, Screen.height / 2 - 84,
        this.win_img.width, this.win_img.height));
    {
        if (this.win_player_index == byte.MaxValue)
        {
            GUI.DrawTexture(new Rect(00this.draw_img.width, this.draw_img.height), this.draw_img);
        }
        else
        {
            // win, lose 이미지 출력
            if (this.player_me_index == this.win_player_index)
            {
                GUI.DrawTexture(new Rect(00346169), this.win_img);
            }
            else
            {
                GUI.DrawTexture(new Rect(00346169), this.lose_img);
            }
        }
 
        // 자기 자신의 플레이어 이미지 출력
        Texture character = this.img_players[this.player_me_index];
        GUI.DrawTexture(new Rect(2843, character.width, character.height), character);
    }
    GUI.EndGroup();
}
cs

on_gui_game_result 메소드는 게임 결과 화면을 출력하는 코드로 구성되어 있습니다.
첫번째 줄에서 on_gui_playing 메소드를 호출해주는 이유는 게임 플레이 화면을 밑 바닥에 깔고
그 위에 결과 화면을 출력해주기 위함입니다. 게임은 끝났지만 보드 판의 상태가 어떤 모습이었는지
계속 보고 싶을 수 있기 때문이죠. 물론 원한다면 on_gui_playing 메소드 호출은 제거해도 상관 없는 부분입니다.



화면 전체를 어두운 배경으로 덮은 뒤 승, 패 여부에 따라 승리, 패배 이미지를 출력해줍니다.
결과 화면에서 나올 수 있는 상황은 총 세 가지 입니다.

첫째 무승부.
둘째 본인의 승리.
셋째 본인의 패배(상대방의 승리).

만약 this.win_player_index의 값이 byte.MaxValue와 같다면 무승부라는 뜻입니다.
승리한 플레이어가 없으니 플레이어 인덱스로 사용된 byte 자료형의 최대 값을 넣어서 무승부를 표현한 것입니다.
만약 this.win_player_index의 값이 this.player_me_index와 같다면 본인이 승리한 경우입니다.
위 두 조건이 아닌 경우에는 상대방의 승리입니다.
승패에 따라서 결과 이미지를 출력해준 뒤 자기 자신의 캐릭터를 그 위에 출력하여 내가 승리했는지 패배했는지 알 수 있도록 해줍니다.

메인 화면으로 복귀
게임의 승패를 확인한 뒤에는 다시 메인화면으로 복귀할 수 있도록 처리해줘야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Update()
{
    if (this.is_game_finished)
    {
        if (Input.GetMouseButtonDown(0))
        {
            back_to_main();
        }
    }
}
 
void back_to_main()
{
    this.main_title.gameObject.SetActive(true);
    this.main_title.enter();
 
    gameObject.SetActive(false);
}
cs

Update 메소드에서 게임이 종료된 상태일 경우 터치 입력을 감지하여 메인 화면으로 복귀하도록 해줍니다.
back_to_main 메소드에서는 MainTitle 오브젝트를 활성화시키고 현재 오브젝트(BattleRoom)을 비활성화하여
게임 방에서 퇴장하도록 처리합니다.

여기까지 클라이언트 로직 구현을 살펴봤습니다.
다음 시간에는 서버쪽 로직 구현을 통해서 클라이언트의 요청을 서버에서 어떻게 처리하는지 알아보도록 하겠습니다.
이제 거의 끝이 보이는군요.


반응형

'- Programming > - C#' 카테고리의 다른 글

★ 14. c# 네트워크 개발 p13  (0) 2017.02.16
★ 13. c# 네트워크 개발 p12  (0) 2017.02.16
★ 11. c# 네트워크 개발 p10  (0) 2017.02.15
★ 10. c# 네트워크 개발 p9  (0) 2017.02.14
★ 9. c# 네트워크 개발 p8  (1) 2017.02.13