Многопользовательская игра измерения времени реакции пользователя - Студенческий научный форум

XI Международная студенческая научная конференция Студенческий научный форум - 2019

Многопользовательская игра измерения времени реакции пользователя

Чиркин К.Д. 1
1Пензенский государственный университет
 Комментарии
Текст работы размещён без изображений и формул.
Полная версия работы доступна во вкладке "Файлы работы" в формате PDF

Введение

Попробуем представить себе мир примерно тридцать пять - сорок лет назад. Мир без общедоступных компьютерных сетей. Мир, в котором каждый компьютер должен был иметь собственное хранилище данных и собственный принтер. Мир, в котором не было электронной почты и систем обмена мгновенными сообщениями (например, ICQ). Как ни странно это звучит сейчас, но до появления компьютерных сетей все это было именно так.

Компьютеры - важная часть сегодняшнего мира, а компьютерные сети серьезно облегчают нашу жизнь, ускоряя работу и делая отдых более интересным.

Практически сразу после появления ЭВМ возник вопрос о налаживании взаимодействия компьютеров друг с другом, чтобы более эффективно обрабатывать информацию, использовать программные и аппаратные ресурсы. Появились и первые сети, в то время объединявшие только большие ЭВМ в крупных компьютерных центрах. Однако настоящий "сетевой бум" начался после появления персональных компьютеров, быстро ставших доступными широкому кругу пользователей - сначала на работе, а затем и дома. Компьютеры стали объединять в локальные сети, а локальные сети - соединять друг с другом, подключать к региональным и глобальным сетям. В результате за последние пятнадцать-двадцать лет сотни миллионов компьютеров в мире были объединены в сети, и более миллиарда пользователей получили возможность взаимодействовать друг с другом, в том числе играть в сетевые игры.

Постановка задачи

Требуется разработать систему программ клиент-серверной архитектуры, позволяющую определить время реакции пользователя в игровой форме на основе измерения задержки между появлением объекта на экране и кликом по нему. Предусмотреть возможность многопользовательского режима работы системы программ в рамках ЛВС.

Функции сервера: учет клиентов и пользователей, генерация синхронных событий для клиента, взаимодействие с клиентом.

Функции клиента: визуальный интерфейс пользователя, отображение синхро-событий на экране и измерение времени реакции пользователя.

Операционная система: Windows.

Языки программирования: C, C++,C#.

Теоретическая часть задания.

Архитектурный принцип построения сетей (за исключением одноранговых сетей, в которых компьютеры равноправны) называется "клиент - сервер".

В одноранговой сети все компьютеры равноправны. Каждый из них может выступать как в роли сервера, т. е. предоставлять файлы и аппаратные ресурсы (накопители, принтеры и пр.) другим компьютерам, так и в роли клиента, пользующегося ресурсами других компьютеров. Например, если на вашем компьютере установлен принтер, то с его помощью смогут распечатывать свои документы все остальные пользователи сети, а вы, в свою очередь, сможете работать с Интернетом, подключение к которому осуществляется через соседний компьютер.

Важнейшими понятиями теории сетей "клиент-сервер" являются "абонент", "сервер", "клиент".

Абонент (узел, хост, станция) - это устройство, подключенное к сети и активно участвующее в информационном обмене. Чаще всего абонентом (узлом) сети является компьютер, но абонентом также может быть, например, сетевой принтер или другое периферийное устройство, имеющее возможность напрямую подключаться к сети.

Сервером называется абонент (узел) сети, который предоставляет свои ресурсы другим абонентам, но сам не использует их ресурсы. Таким образом, он обслуживает сеть. Серверов в сети может быть несколько, и совсем не обязательно, что сервер - самый мощный компьютер. Выделенный (dedicated) сервер - это сервер, занимающийся только сетевыми задачами. Невыделенный сервер может помимо обслуживания сети выполнять и другие задачи. Специфический тип сервера - это сетевой принтер.

Клиентом называется абонент сети, который только использует сетевые ресурсы, но сам свои ресурсы в сеть не отдает, то есть сеть его обслуживает, а он ей только пользуется. Компьютер-клиент также часто называют рабочей станцией. В принципе каждый компьютер может быть одновременно как клиентом, так и сервером.

Под сервером и клиентом часто понимают также не сами компьютеры, а работающие на них программные приложения. В этом случае то приложение, которое только отдает ресурс в сеть, является сервером, а то приложение, которое только пользуется сетевыми ресурсами - клиентом.

Основу сети Интернет составляет группа протоколов TCP/IP.

Протокол TCP (Transmission Control Protocol) - транспортного уровня, он управляет тем, как происходит передача информации (данные "нарезаются" на пакеты и маркируются).

IP (Internet Protocol) - протокол сетевого уровня, добавляет к пакету IP-адреса получателя и отравителя и отвечает на вопрос, как проложить маршрут для доставки информации.

Каждый компьютер, включенный в сеть - хост, имеет свой уникальный IP-адрес. Этот адрес выражается четырьмя байтами, например: 234.049.122.201, и регистрируется в Информационном центре сети- InterNIC или в Network Solutions Inc (NSI). Организация IP-адреса такова, что каждый компьютер, через который проходит TCP-пакет, может определить, кому из ближайших "соседей" его нужно переслать.

Описание алгоритма программы.

Разработанная программа основывается на вышеописанных протоколах. Она состоит из клиента и сервера.

Вначале нужно запустить сервер и ввести количество игроков для начала игры и количество «раундов стрельбы», после чего сервер начнет прослушивать порт на наличие клиентов. Когда подключается клиент, то он добавляется в общий словарь клиентов и под него выделяется отдельная нить, в которой происходит прослушивание данных с клиента.

После успешного подключения клиент запрашивает у пользователя никнейм в специальном окне и отправляет введенный текст на сервер. Если такой никнейм уже использован другим пользователем, то клиент получит ошибку и предложит пользователю ввести его еще раз.

Если ник принят, то клиент будет находиться в ожидании. Когда к серверу подключится достаточное количество клиентов, то игра начнется и всем клиентам будет отправлена команда, по которой клиенты поймут, что нужно нарисовать мишень. При «выстреле» по мишени клиент отправляет сообщение серверу с измеренным временем реакции данного пользователя в данном раунде. Когда все пользователи совершили выстрелы, то сервер повторяет отправку команд клиентам. Так происходит, пока количество сыгранных раундов не достигнет максимального заданного значения.

Когда игра окончена, то сервер рассылает всем клиентам сообщения с результатами каждого игрока, а также информирует пользователя о том, выиграл ли он или нет. После чего все клиенты отключаются от сервера и могут подключиться заново, чтобы начать новую игру.

Описание программы.

Основные модули.

Программа разбита на несколько модулей. Сервер состоит из модулей:

Program.cs – модуль для конфигурации сервера.

Server.cs – модуль для управления всем сетевым взаимодействием с клиентами

Client.cs – модель клиента

PacketEnums.cs – модуль с перечислениями, обозначающими типы сетевых пакетов

DisconnectPacket.cs – пакет для уведомления об отключении

ErrorPacket.cs – пакет с ошибкой

GameResultsPacket.cs – пакет с результатами игры

NickPacket.cs – пакет с никнеймом игрока

ShootPacket.cs – пакет для уведомления о событии выстрела

Клиент состоит из модулей:

Program.cs – модуль для конфигурации и запуска приложения

Client.cs – модуль для управления всем сетевым взаимодействием с сервером

FormMain.cs – модуль для работы с основной частью графического интерфейса игры

GameResultsDialog.cs – диалоговое окно для вывода результатов игры

NickDialog.cs – диалоговое окно для ввода никнейма игрока.

PacketEnums.cs – модуль с перечислениями, обозначающими типы сетевых пакетов

DisconnectPacket.cs – пакет для уведомления об отключении

ErrorPacket.cs – пакет с ошибкой

GameResultsPacket.cs – пакет с результатами игры

NickPacket.cs – пакет с никнеймом игрока

ShootPacket.cs – пакет для уведомления о событии выстрела

На клиенте основными модулями являются FormMain и Client. FormMain обращается к методам клиента непосредственно через экземпляр класса Client, а тот в свою очередь сообщает форме о событиях посредством callback’ов. Таким образом, достигается разделение ответственности.

Протокол взаимодействия.

Сообщение в протоколе взаимодействия клиента и сервера представляет собой 1 символ, отвечающий за идентификатор сообщения и объект, сериализованный в виде строки JSON (JavaScript Object Notation). То есть пакет с никнеймом выглядит так: “2{“nick”:”kirill”}”.

Список пакетов и их идентификаторов:

ERROR = 1 – пакет с ошибкой

NICK = 2 - пакет с никнеймом игрока

SHOOT = 3 - пакет с командой для клиента, чтобы отобразилась мишень, либо для уведомления сервера о выстреле на клиенте

GAME_RESULTS = 4 - пакет с результатами всей игры

DISCONNECT = 5 – пакет, уведомляющий об отключении

Список ошибок и их идентификаторов:

SUCCESS = 0 – результат успешный

NICK_ALREADY_EXISTS = 1 – никнейм уже существует

GAME_ALREADY_STARTED = 2 – играуженачалась

Схема алгоритма программы.

Продолжение на следующей странице

Рисунок 1 – Блок-схема алгоритма сервера

Продолжение на следующей странице

Рисунок 2 – Блок-схема алгоритма сервера

Описание работы программы.

Вначале нужно запустить сервер. При запуске нужно ввести количество игроков для игры и количество выстрелов в игре. Если ввести неправильное значение вместо числа, то будет запрошен повторный ввод данных:

Рисунок 3 – Запуск сервера

Далее запускаем клиент:

Рисунок 4 – Запуск клиента

При подключении нужно ввести ник:

Рисунок 5 – Ввод ника

Рисунок 6 – Ник принят, клиент в ожидании начала игры

Рисунок 7 – Лог сервера при получении ника

Рисунок 8 – Игра началась, на экране отрисовалась мишень

Побеждает тот, кто совершил самый быстрый выстрел за всю игру. В результатах игры отображается ник победителя, а также результаты всех выстрелов для каждого игрока:

Рисунок 9 – Игра окончена, на экране отображены результаты игры

Заключение

Таким образом, была разработана игра с клиент-серверной архитектурой, позволяющая определить время реакции пользователя в игровой форме посредством стрельбы по мишеням. В процессе написания программы были изучены навыки работы с языком C#, Windows Forms и сокетами.

Список литературы

Система вопросов и ответов о программировании [Электронный ресурс]: URL: https://stackoverflow.com

Документация Microsoft. [Электронный ресурс]: URL: https://docs.microsoft.com

Регулярные выражения в c#. [Электронный ресурс]: URL: https://regexone.com/references/csharp

Краткий видео-урок по c#. [Электронный ресурс]: URL: https://www.youtube.com/watch?v=lisiwUZJXqQ

Приложение А. Листинг программы.

ПриложениеА1Server

Файл «Program.cs»

using System;

namespace ReactionGameServer

{

class Program

{

static void Main(string[] args)

{

Console.Title = "Reaction game server";

int playersCount, shootCount;

while (true)

{

try

{

Console.Write("How many players: ");

var playersCountString = Console.ReadLine();

playersCount = int.Parse(playersCountString);

break;

}

catch

{

Console.WriteLine("invalid value");

}

}

while (true)

{

try

{

Console.Write("How many shoots: ");

var shootCountString = Console.ReadLine();

shootCount = int.Parse(shootCountString);

break;

}

catch

{

Console.WriteLine("invalid value");

}

}

var server = new Server(playersCount, shootCount);

server.Start();

Console.ReadKey();

}

}

}

ПриложениеА2Server

Файл «Server.cs»

using ReactionGameServer.packets;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Net;

using System.Net.Sockets;

using System.Text;

using System.Threading;

using Newtonsoft.Json;

namespace ReactionGameServer

{

class Server

{

public Server(int _maxPlayers, int _maxShoots)

{

maxPlayers = _maxPlayers;

maxShoots = _maxShoots;

}

private bool isGameStarted = false;

private int maxPlayers = 2;

private int maxShoots = 5;

Dictionary<string, Client> clients = new Dictionary<string, Client>();

private Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

private object gameCountersLock = new object();

private int receivedShoots = 0;

private int shootsCount = 0;

public void Start()

{

serverSocket.Bind(new IPEndPoint(IPAddress.Any, 8101));

serverSocket.Listen(maxPlayers);

serverSocket.BeginAccept(AcceptCallback, null);

Console.WriteLine("Server started");

}

private void AcceptCallback(IAsyncResult asyncResult)

{

var clientSocket = serverSocket.EndAccept(asyncResult);

if (isGameStarted)

{

var errorPacket = new ErrorPacket((int)ErrorType.GAME_ALREADY_STARTED, "game already started", 0);

var errorJson = SerializePacket(PacketType.ERROR, errorPacket);

clientSocket.Send(Encoding.UTF8.GetBytes(errorJson));

} else

{

var clientThread = new Thread(HandleClient);

var client = new Client(clientSocket);

var clientId = Client.GenerateId();

lock (clients)

{

clients.Add(clientId, client);

}

clientThread.Start(clientId);

}

serverSocket.BeginAccept(AcceptCallback, null);

}

private void HandleClient(object obj)

{

var clientId = (string)obj;

var client = clients[clientId];

var clientSocket = client.socket;

Console.WriteLine("client connected");

var buffer = new byte[1024];

var isReceiving = true;

try

{

do

{

int receivedBytesCount = clientSocket.Receive(buffer);

if (receivedBytesCount == 0)

{

continue;

}

var jsonWithId = Encoding.UTF8.GetString(buffer);

var packetType = GetPacketType(jsonWithId);

var json = GetActualJson(jsonWithId);

switch (packetType)

{

case PacketType.DISCONNECT:

isReceiving = false;

break;

case PacketType.NICK:

var nickPacket = JsonConvert.DeserializeObject<NickPacket>(json);

var isNickAdded = HandleNickPacket(clientId, nickPacket);

if (isNickAdded)

{

TryStartGame();

}

break;

case PacketType.SHOOT:

var shootPacket = JsonConvert.DeserializeObject<ShootPacket>(json);

HandleShootPacket(clientId, shootPacket);

break;

default:

Console.WriteLine("Unknown packet");

break;

}

Array.Clear(buffer, 0, buffer.Length);

} while (clientSocket.Connected && isReceiving);

}

catch (Exception e)

{

Console.WriteLine(e.Message);

Console.WriteLine(e.StackTrace);

}

finally

{

DisconnectClient(clientId);

}

}

private void DisconnectClient(string clientId)

{

try

{

var client = clients[clientId];

client.socket.Disconnect(true);

client.socket.Close();

}

catch { }

lock (clients)

{

clients.Remove(clientId);

}

Console.WriteLine("Client disconnected");

}

private bool HandleNickPacket(string clientId, NickPacket nickPacket)

{

bool isNickAvailable = true;

Client client;

lock (clients)

{

client = clients[clientId];

foreach (Client otherClient in clients.Values)

{

if (nickPacket.nick.Equals(otherClient.nick))

{

isNickAvailable = false;

break;

}

}

}

if (isNickAvailable)

{

client.nick = nickPacket.nick;

lock (clients)

{

clients[clientId] = client;

}

Console.WriteLine("Received packet: NickPacket, nick = " + nickPacket.nick);

var errorPacket = new ErrorPacket((int)ErrorType.SUCCESS, "nick added", PacketType.NICK);

var json = SerializePacket(PacketType.ERROR, errorPacket);

client.socket.Send(Encoding.UTF8.GetBytes(json));

return true;

}

else

{

var errorPacket = new ErrorPacket((int)ErrorType.NICK_ALREADY_EXISTS, "nick already exists", PacketType.NICK);

var json = SerializePacket(PacketType.ERROR, errorPacket);

client.socket.Send(Encoding.UTF8.GetBytes(json));

return false;

}

}

private void HandleShootPacket(string clientId, ShootPacket shootPacket)

{

Console.WriteLine("Received packet: ShootPacket");

Client client;

lock (clients)

{

client = clients[clientId];

client.shootDeltas.Add(shootPacket.shootDelta);

clients[clientId] = client;

}

lock (gameCountersLock)

{

receivedShoots++;

}

var clientsCount = 0;

lock (clients)

{

clientsCount = clients.Count;

}

lock (gameCountersLock)

{

if (receivedShoots >= clientsCount && shootsCount < maxShoots)

{

SendShootPackets();

receivedShoots = 0;

}

else if (receivedShoots >= clientsCount && shootsCount >= maxShoots)

{

Console.WriteLine("Game finished");

SendGameResults();

ResetGame();

}

}

}

private void TryStartGame()

{

int clientsCount = 0;

lock (clients)

{

clientsCount = clients.Count;

}

if (clientsCount == maxPlayers)

{

//starting a game

SendShootPackets();

lock (gameCountersLock)

{

isGameStarted = true;

}

Console.WriteLine("Game started");

}

else if (clientsCount > maxPlayers)

{

throw new InvalidOperationException("clients count larger than maxPlayers value");

}

}

private void SendShootPackets()

{

lock (gameCountersLock)

{

shootsCount++;

}

var shootPacket = new ShootPacket();

var json = SerializePacket(PacketType.SHOOT, shootPacket);

var encodedJson = Encoding.UTF8.GetBytes(json);

lock (clients)

{

foreach (Client client in clients.Values)

{

client.socket.Send(encodedJson);

}

}

Console.WriteLine("Sent shoot packets");

}

private void SendGameResults()

{

var gameResults = new List<GameResult>();

lock (clients)

{

foreach (Client client in clients.Values)

{

var gameResult = new GameResult(client.nick, client.shootDeltas);

gameResults.Add(gameResult);

}

}

var nickOfWinner = CalculateWinnerNick(gameResults);

var gameResultsPacket = new GameResultsPacket(gameResults, nickOfWinner);

var json = SerializePacket(PacketType.GAME_RESULTS, gameResultsPacket);

var encodedJson = Encoding.UTF8.GetBytes(json);

lock (clients)

{

foreach (Client client in clients.Values)

{

client.socket.Send(encodedJson);

}

}

}

private string CalculateWinnerNick(List<GameResult> gameResults)

{

var minDeltas = new Dictionary<string, double>();

foreach (GameResult gameResult in gameResults)

{

var minDelta = gameResult.shootDeltas.Min();

minDeltas.Add(gameResult.nick, minDelta);

}

KeyValuePair<string, double> winnerPair = new KeyValuePair<string, double>("", double.MaxValue);

foreach (KeyValuePair<string, double> pair in minDeltas)

{

if (pair.Value < winnerPair.Value)

{

winnerPair = pair;

}

}

return winnerPair.Key;

}

private void ResetGame()

{

lock (gameCountersLock)

{

isGameStarted = false;

receivedShoots = 0;

shootsCount = 0;

}

var disconnectPacket = new DisconnectPacket();

var json = SerializePacket(PacketType.DISCONNECT, disconnectPacket);

var encodedJson = Encoding.UTF8.GetBytes(json);

lock (clients)

{

foreach (Client client in clients.Values)

{

client.socket.Send(encodedJson);

}

clients.Clear();

}

Console.WriteLine("Game reset");

}

private PacketType GetPacketType(string json)

{

var packetIdString = json.Substring(0, 1); // берем первые 2 символа, в котором хранится id

var packetId = int.Parse(packetIdString);

return (PacketType)packetId;

}

private string GetActualJson(string jsonWithId)

{

return jsonWithId.Substring(1, jsonWithId.Length - 1);

}

private string SerializePacket(PacketType packetType, object packet)

{

var json = JsonConvert.SerializeObject(packet);

return (int)packetType + json;

}

}

}

Приложение А3Server

Файл «Client.cs»

using System;

using System.Collections.Generic;

using System.Net.Sockets;

namespace ReactionGameServer

{

public class Client

{

public static string GenerateId()

{

return Guid.NewGuid().ToString();

}

public Client(Socket _socket)

{

socket = _socket;

}

public Socket socket;

public string nick;

public List<double> shootDeltas = new List<double>();

public int wonRounds = 0;

}

}

ПриложениеА4Server

Файл «DisconnectPacket.cs»

namespace ReactionGameServer.packets

{

classDisconnectPacket

{

}

}

ПриложениеА5Server

Файл «ErrorPacket.cs»

namespace ReactionGameServer.packets

{

public class ErrorPacket

{

public ErrorPacket(int _errId, string _message, PacketType _previousPacketType)

{

errId = _errId;

message = _message;

previousPacketType = (int)_previousPacketType;

}

public int errId;

public string message;

public int previousPacketType;

}

}

ПриложениеА6Server

Файл «ErrorPacket.cs»

using System.Collections.Generic;

namespace ReactionGameServer.packets

{

public class GameResultsPacket

{

public GameResultsPacket(List<GameResult> _gameResults, string _nickOfWinner)

{

gameResults = _gameResults;

nickOfWinner = _nickOfWinner;

}

public List<GameResult> gameResults;

public string nickOfWinner;

}

public class GameResult

{

public GameResult(string _nick, List<double> _shootDeltas)

{

nick = _nick;

shootDeltas = _shootDeltas;

}

public string nick;

public List<double> shootDeltas;

}

}

ПриложениеА7Server

Файл «NickPacket.cs»

namespace ReactionGameServer.packets

{

public class NickPacket

{

public NickPacket(string _nick)

{

nick = _nick;

}

public string nick;

}

}

ПриложениеА8Server

Файл «ShootPacket.cs»

namespace ReactionGameServer.packets

{

public class ShootPacket

{

public ShootPacket()

{

//empty

}

public ShootPacket(double _shootDelta)

{

shootDelta = _shootDelta;

}

public double shootDelta = 0;

}

}

ПриложениеА9Server

Файл «PacketEnums.cs»

namespace ReactionGameServer.packets

{

public enum PacketType

{

ERROR = 1,

NICK = 2, // пакет с никнеймом игрока

SHOOT = 3, // пакет с командой для клиента, чтобы отобразилась мишень

GAME_RESULTS = 4, // пакет с результатами всей игры

DISCONNECT = 5

}

public enum ErrorType

{

SUCCESS = 0,

NICK_ALREADY_EXISTS = 1,

GAME_ALREADY_STARTED = 2

}

}

Приложение А1Client

Файл «Program.cs»

using System;

using System.Windows.Forms;

namespace ReactionGameClient

{

static class Program

{

private const string defaultHost = "localhost";

private const short defaultPort = 8107;

[STAThread]

static void Main()

{

Application.EnableVisualStyles();

Application.SetCompatibleTextRenderingDefault(false);

var mainForm = new FormMain()

{

StartPosition = FormStartPosition.CenterScreen

};

Application.Run(mainForm);

}

}

}

Приложение А2Client

Файл «FormMain.cs»

using ReactionGameClient.packets;

using System;

using System.Diagnostics;

using System.Drawing;

using System.Text.RegularExpressions;

using System.Threading.Tasks;

using System.Windows.Forms;

namespace ReactionGameClient

{

public partial class FormMain : Form

{

private const int TARGET_MARGIN = 10; //pixels

private const int TARGET_SIZE = 50; //pixels

private readonly Bitmap bitmap;

private readonly Graphics canvas;

private int targetX = 0, targetY = 0;

private readonly Random random = new Random();

private bool isShootNow = false;

private Stopwatch clock = new Stopwatch();

private readonly Client client;

private readonly Regex REGEX_IP = new Regex(

@"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]):[0-9]+$",

RegexOptions.Compiled

);

public FormMain()

{

InitializeComponent();

// initialization

bitmap = new Bitmap(GameDrawPanel.Width, GameDrawPanel.Height);

canvas = Graphics.FromImage(bitmap);

client = new Client()

{

onConnected = OnConnected,

onDisconnected = OnDisconnected,

onErrorReceived = OnErrorReceived,

onNickRequested = OnNickRequested,

onNickReceived = OnNickStatusReceived,

onShootReceived = OnShootReceived,

onGameResultReceived = OnGameResultsReceived,

onGameAlreadyStarted = OnGameAlreadyStarted

};

}

private void OnConnected()

{

ConnectDisconnectButton.Text = "Disconnect";

}

private void OnDisconnected()

{

this.Invoke(new Action(() =>

{

ClearTarger();

ConnectDisconnectButton.Text = "Connect";

infoLabel.Text = "Disconnected";

}));

}

private void OnErrorReceived(ErrorPacket error)

{

MessageBox.Show(error.message);

}

private void OnNickRequested()

{

GetNick();

}

private void OnNickStatusReceived(bool isSuccess)

{

if (!isSuccess)

{

GetNick(false);

}

else

{

ShowWaitForOtherPlayersDialog();

}

}

private void OnShootReceived()

{

ClearTarger();

DrawTarget();

}

private void OnGameResultsReceived(GameResultsPacket gameResultsPacket)

{

var dialog = new GameResultsDialog()

{

StartPosition = FormStartPosition.CenterParent,

};

dialog.SetResults(gameResultsPacket);

dialog.ShowDialog();

}

private async void OnGameAlreadyStarted()

{

MessageBox.Show("Game already started");

await Disconnect();

}

private void GetNick(bool isNickSuccess = true)

{

var nick = GetNickFromUser(isNickSuccess);

if (nick == null)

{

client.DisconnectAsync();

}

else

{

client.SendNick(nick);

}

}

private void DrawTarget()

{

var targetBitmap = Properties.Resources.Target;

this.Invoke(new Action(() =>

{

canvas.DrawImage(targetBitmap, GenerateTargetPosition());

GameDrawPanel.BackgroundImage = bitmap;

GameDrawPanel.Invalidate();

GameDrawPanel.Update();

infoLabel.Text = "SHOOT!";

isShootNow = true;

clock.Start();

}

));

}

private void ClearTarger()

{

this.Invoke(new Action(() =>

{

canvas.Clear(Color.White);

GameDrawPanel.Invalidate();

}));

}

private Rectangle GenerateTargetPosition()

{

targetX = random.Next(TARGET_MARGIN, GameDrawPanel.Width - TARGET_MARGIN - TARGET_SIZE);

targetY = random.Next(TARGET_MARGIN, GameDrawPanel.Height - TARGET_MARGIN - TARGET_SIZE);

return new Rectangle(targetX, targetY, TARGET_SIZE, TARGET_SIZE);

}

private void ShowWaitForOtherPlayersDialog()

{

infoLabel.Invoke(new Action(() => infoLabel.Text = "Please, wait for other players"));

}

private void ClearInfoLabel()

{

infoLabel.Invoke(new Action(() => infoLabel.Text = ""));

}

// may return null

private string GetNickFromUser(bool _isNickSuccess)

{

var nickDialog = new NickDialog()

{

StartPosition = FormStartPosition.CenterParent

};

if (!_isNickSuccess)

{

nickDialog.ShowNickNotAdded();

}

nickDialog.ShowDialog();

return nickDialog.nick;

}

private async Task Connect()

{

var ipAndPort = ipAndPortTextBox.Text;

var match = REGEX_IP.Match(ipAndPort);

if (match.Success)

{

try

{

var values = ipAndPort.Split(':');

var ip = values[0];

var port = int.Parse(values[1]);

await client.ConnectAsync(ip, port);

}

catch

{

MessageBox.Show("Connection error");

}

}

else

{

MessageBox.Show("Invalid address");

}

}

private async Task Disconnect()

{

await client.DisconnectAsync();

}

private async void OnConnectDisconnectClick(object sender, EventArgs e)

{

if (client.isConnected)

{

await Disconnect();

}

else

{

await Connect();

}

}

private void OnHelpClick(object sender, EventArgs e)

{

MessageBox.Show("Это игра для проверки вашей скорости реакции.\n" +

"\"Стреляй\" по мишеням мышкой как можно быстрее!");

}

private void GameDrawPanel_MouseClick(object sender, MouseEventArgs e)

{

if (isShootNow

&& e.X >= targetX && e.X <= (targetX + TARGET_SIZE)

&& e.Y >= targetY && e.Y <= (targetY + TARGET_SIZE))

{

// clicked on target

clock.Stop();

var deltaTime = clock.Elapsed.TotalSeconds;

clock.Reset();

client.SendShoot(deltaTime);

ClearTarger();

ShowWaitForOtherPlayersDialog();

infoLabel.Text = "Get ready for the next shoot";

isShootNow = false;

}

}

}

}

Приложение А3Client

Файл «Client.cs»

using Newtonsoft.Json;

using ReactionGameClient.packets;

using System;

using System.Diagnostics;

using System.Net.Sockets;

using System.Text;

using System.Threading;

using System.Threading.Tasks;

namespace ReactionGameClient

{

public class Client

{

private const string defaultHost = "localhost";

private const int defaultPort = 8101;

public bool isConnected = false;

public Action onConnected;

public Action onDisconnected;

public Action<ErrorPacket> onErrorReceived;

public Action onNickRequested;

public Action<bool> onNickReceived;

public Action onShootReceived;

public Action<GameResultsPacket> onGameResultReceived;

public Action onGameAlreadyStarted;

private Socket socket = null;

public async Task ConnectAsync(string host = defaultHost, int port = defaultPort)

{

await Task.Factory.StartNew(() =>

{

socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

socket.Connect(host, port);

});

isConnected = true;

onConnected.Invoke();

onNickRequested.Invoke();

}

public async Task DisconnectAsync()

{

await Task.Factory.StartNew(() =>

{

try

{

if (isConnected)

{

SendDisconnect();

socket.Shutdown(SocketShutdown.Both);

socket.Close();

}

}

catch (Exception e)

{

Debug.WriteLine(e.Message);

Debug.WriteLine(e.StackTrace);

}

finally

{

isConnected = false;

onDisconnected.Invoke();

}

});

}

public void SendNick(string nick)

{

if (socket == null)

{

throw new InvalidOperationException("socket == null");

}

var nickPacket = new NickPacket(nick);

var nickJson = JsonConvert.SerializeObject(nickPacket);

nickJson = (int)PacketType.NICK + nickJson;

socket.Send(Encoding.UTF8.GetBytes(nickJson));

var receiveThread = new Thread(ServerReceiveHandler);

receiveThread.Start();

}

public void SendShoot(double shootDelta)

{

if (socket == null)

{

throw new InvalidOperationException("socket == null");

}

var shootPacket = new ShootPacket(shootDelta);

var shootJson = JsonConvert.SerializeObject(shootPacket);

shootJson = (int)PacketType.SHOOT + shootJson;

socket.Send(Encoding.UTF8.GetBytes(shootJson));

}

private void SendDisconnect()

{

if (socket == null)

{

throw new InvalidOperationException("socket == null");

}

var disconnectPacket = new DisconnectPacket();

var disconnectJson = JsonConvert.SerializeObject(disconnectPacket);

disconnectJson = (int)PacketType.DISCONNECT + disconnectJson;

socket.Send(Encoding.UTF8.GetBytes(disconnectJson));

}

private void ServerReceiveHandler(object obj)

{

var buffer = new byte[1024];

try

{

do

{

int receivedBytesCount = socket.Receive(buffer);

if (receivedBytesCount == 0)

{

continue;

}

var jsonWithId = Encoding.UTF8.GetString(buffer);

var packetType = GetPacketType(jsonWithId);

var receivedJson = GetActualJson(jsonWithId);

switch (packetType)

{

case PacketType.ERROR:

var error = JsonConvert.DeserializeObject<ErrorPacket>(receivedJson);

HandleError(socket, error);

break;

case PacketType.SHOOT:

onShootReceived.Invoke();

break;

case PacketType.GAME_RESULTS:

var gameResultsPacket = JsonConvert.DeserializeObject<GameResultsPacket>(receivedJson);

onGameResultReceived.Invoke(gameResultsPacket);

break;

case PacketType.DISCONNECT:

onDisconnected.Invoke();

isConnected = false;

break;

default:

throw new ArgumentException("Unknown package");

}

Array.Clear(buffer, 0, buffer.Length);

} while (socket.Connected && isConnected);

}

catch (Exception e)

{

Debug.WriteLine(e.Message);

Debug.WriteLine(e.StackTrace);

}

finally

{

try

{

SendDisconnect();

socket.Shutdown(SocketShutdown.Both);

socket.Close();

}

catch (Exception e)

{

Debug.WriteLine(e.Message);

Debug.WriteLine(e.StackTrace);

}

finally

{

isConnected = false;

onDisconnected();

}

}

}

private void HandleError(Socket socket, ErrorPacket error)

{

switch (error.previousPacketType)

{

case (int)PacketType.NICK:

switch (error.errId)

{

case (int)ErrorType.NICK_ALREADY_EXISTS:

onNickReceived.Invoke(false);

break;

case (int)ErrorType.GAME_ALREADY_STARTED:

onGameAlreadyStarted.Invoke();

break;

case (int)ErrorType.SUCCESS:

onNickReceived.Invoke(true);

break;

default:

throw new ArgumentException("Unknown error type");

}

break;

default:

onErrorReceived.Invoke(error);

break;

}

}

private PacketType GetPacketType(string json)

{

var packetIdString = json.Substring(0, 1); // берем первый символ, в котором хранится id

var packetId = int.Parse(packetIdString);

return (PacketType)packetId;

}

private string GetActualJson(string jsonWithId)

{

return jsonWithId.Substring(1, jsonWithId.Length - 1);

}

}

}

ПриложениеА4Client

Файл «NickDialog.cs»

using System;

using System.Windows.Forms;

namespace ReactionGameClient

{

public partial class NickDialog : Form

{

public NickDialog()

{

InitializeComponent();

}

public string nick = null;

private void OnNickApplyClick(object sender, EventArgs e)

{

var nick = NickTextBox.Text;

if (nick.Length == 0)

{

MessageBox.Show("Cannot apply empty nick");

} else

{

this.nick = nick;

Close();

}

}

public void ShowNickNotAdded()

{

this.Text = "This nick already exists";

}

}

}

ПриложениеА5Client

Файл «GameResultsDialog.cs»

using ReactionGameClient.packets;

using System.Text;

using System.Windows.Forms;

namespace ReactionGameClient

{

public partial class GameResultsDialog : Form

{

public GameResultsDialog()

{

InitializeComponent();

}

public void SetResults(GameResultsPacket gameResultPacket)

{

winnerLabel.Text = "Winner: " + gameResultPacket.nickOfWinner;

var gameResultsString = new StringBuilder();

foreach (GameResult result in gameResultPacket.gameResults)

{

gameResultsString.Append("Nick: ");

gameResultsString.Append(result.nick);

gameResultsString.Append("\n");

gameResultsString.Append("Shoot deltas: ");

gameResultsString.Append("\n");

foreach (double delta in result.shootDeltas)

{

gameResultsString.Append(delta);

gameResultsString.Append(" sec");

gameResultsString.Append("\n");

}

gameResultsString.Append("\n");

}

resultsTextBox.Text = gameResultsString.ToString();

}

}

}

ПриложениеА6Client

Файл «DisconnectPacket.cs»

namespace ReactionGameClient.packets

{

class DisconnectPacket

{

}

}

ПриложениеА7Client

Файл «ErrorPacket.cs»

namespace ReactionGameClient.packets

{

public class ErrorPacket

{

public ErrorPacket(int _errId, string _message, PacketType _previousPacketType)

{

errId = _errId;

message = _message;

previousPacketType = (int)_previousPacketType;

}

public int errId;

public string message;

public int previousPacketType;

}

}

ПриложениеА8Client

Файл «GameResultsPacket.cs»

using System.Collections.Generic;

namespace ReactionGameClient.packets

{

public class GameResultsPacket

{

public GameResultsPacket(List<GameResult> _gameResults, string _nickOfWinner)

{

gameResults = _gameResults;

nickOfWinner = _nickOfWinner;

}

public List<GameResult> gameResults;

public string nickOfWinner;

}

public class GameResult

{

public GameResult(string _nick, List<double> _shootDeltas)

{

nick = _nick;

shootDeltas = _shootDeltas;

}

public string nick;

public List<double> shootDeltas;

}

}

ПриложениеА9Client

Файл «NickPacket.cs»

namespace ReactionGameClient.packets

{

public class NickPacket

{

public NickPacket(string _nick)

{

nick = _nick;

}

public string nick;

}

}

ПриложениеА10Client

Файл «ShootPacket.cs»

namespace ReactionGameClient.packets

{

public class ShootPacket

{

public ShootPacket()

{

//empty

}

public ShootPacket(double _shootDelta)

{

shootDelta = _shootDelta;

}

public double shootDelta = -1;

}

}

ПриложениеА11Client

Файл «PacketEnums.cs»

namespace ReactionGameClient.packets

{

public enum PacketType

{

ERROR = 1,

NICK = 2, // пакет с никнеймом игрока

SHOOT = 3, // пакет с командой для клиента, чтобы отобразилась мишень

GAME_RESULTS = 4, // пакет с результатами всей игры

DISCONNECT = 5

}

public enum ErrorType

{

SUCCESS = 0,

NICK_ALREADY_EXISTS = 1,

GAME_ALREADY_STARTED = 2

}

}

Просмотров работы: 15