И всё же игра!
Всем снова привет! Рада что вы читаете это, ведь наша история о споре подходит к финальной стадии.
В прошлой статье я сделала наброски кода и спустя несколько дней (благодаря советам более опытных программистов) готова вам показать полностью переписанный с нуля код с объяснениями.
Скачать готовый код можно в конце статьи из моего рипозитория (если не можете дождаться).
Начнём с начала, разберём начальные объекты
Тут мы разберём что будет в нашем приложении и что к этому будем заматывать изолентой (конечно же кодом).
1-е, это конечно стены (Walls)
2-е, это наши игроки (Players)
3-е, снаряды (Shots)
Возникает вопрос, а как это систематизировать и заставить работать вместе?
Что способно создать систему? Конечно же структура, но у любой структуры должны быть параметры. Сейчас мы создадим первые из них — это координаты. Для их удобного отображения мы будем использовать следующий класс:
// Класс для удобного представления координат
public class Position
{
// Публичные свойства класса
public int X { get; set; }
public int Y { get; set; }
public Position(int x, int y)
{
X = x;
Y = y;
}
}
Второй параметр — это отличительный номер, ведь каждый элемент структуры должен быть различен, поэтому в любой нашей структуре (внутри неё) обязательно будет поле ID.
Приступим к созданию структур на основе этого класса.
Далее я опишу структуры а потом и их взаимодействие.
Структура 'Игроки' (PlayerState)
Я выделила их отдельно, так как в них огромное количество методов и они очень важны (кто ещё будет играть и двигать модельками?).
Я просто скину поля и ниже начну их описывать:
private int ID { get; set; }
private Position Position { get; set; }
private Position LastPosition { get; set; }
private int[] Collider_X { get; set; }// коллайдер
private int[] Collider_Y { get; set; }
private int hp { get; set; }
//стартовое положение
static int dir;
ID — это я уже успела объяснить
Position — это экземпляр одноимённого класса, LastPosition — предыдущая позиция
Collider — это коллайдер (те точки, попав в которые тебе отнимется здоровье)
Структура 'Игроки', должна содержать метода по обработке экземпляров структуры/подготовке нашего экземпляра к отправке на сервер, для этих задач мы используем следующие методы:
public static void hp_minus(PlayerState player, int hp)
public static void NewPosition(PlayerState player, int X, int Y)
private static bool ForExeption(Position startPosition)
public static ShotState CreateShot(PlayerState player, int dir_player, int damage)
public static void WriteToLastPosition(PlayerState player, string TEXT)
Первый метод — это метод на отнятие определённого количества здоровья у игрока.
Второй метод необходим для назначения новой позиции игрокам.
Третий мы используем в конструкторе, чтобы не было ошибок при создании танка.
Четвёртый создаёт выстрел (т.е. стреляет пулей из танка).
Пятый должен печатать текст в предыдущую позицию игрока (не обновлять же нам каждый кадр экран консоли путём вызова Console.Clear()).
Теперь по каждому методу отдельно, то есть разберём их код:
1-й:
/// <summary>
/// Минус хп
/// </summary>
/// <param name="player">Игрок</param>
/// <param name="hp">Сколько хп отнимаем</param>
public static void hp_minus(PlayerState player, int hp)
{
player.hp -= hp;
}
Я думаю что тут не много придётся объяснять, эта запись оператора полностью эквивалентна вот этой:
player.hp = player.hp - hp;
Остального же в этом методе не будет добавлено.
2-й:
/// <summary>
/// Назначаем другим игрокам позиции
/// </summary>
/// <param name="player">Игрок</param>
/// <param name="X">Координата X</param>
/// <param name="Y">Координата Y</param>
public static void NewPosition(PlayerState player, int X, int Y)
{
if ((X > 0 && X < Width) && (Y > 0 && Y < Height))
{
player.LastPosition = player.Position;
player.Position.X = X;
player.Position.Y = Y;
player.Collider_X = new int[3];
player.Collider_Y = new int[3];
player.Collider_Y[0] = Y; player.Collider_Y[1] = Y + 1; player.Collider_Y[2] = Y + 2;
player.Collider_X[0] = X; player.Collider_X[1] = X + 1; player.Collider_X[2] = X + 2;
}
}
Тут мы комбинируем условия для того чтобы другой игрок (у нас на консоли, вдруг лаги будут) не смог уехать за игровое поле. Кстати поля мы используем (Height и Width) они обозначают границы нашего поля (Высота и Ширина).
3-й:
private static bool ForExeption(Position startPosition)
{
if (startPosition.X > 0 && startPosition.Y > 0) return true;
return false;
}
Тут же мы не даём координатам быть меньше размера игрового поля.
К слову: в консоли система координат начинается с верхнего левого угла (точка 0;0 ) и есть ограничение (которое тоже можно настроить) в 80 по х и 80 по y. Если мы достигаем 80 по х, то мы крашимся (то есть приложение ломается), а если 80 по у, то просто увеличивается размер поля (настроить эти ограничения можно нажав пкм по консоли и выбрав свойства).
5-й:
public static void WriteToLastPosition(PlayerState player, string TEXT)
{
Console.CursorLeft = player.LastPosition.X; Console.CursorTop = player.LastPosition.Y;
Console.Write(TEXT);
}
Тут мы просто печатаем текст в предыдущую позицию (закрашиваем её).
Четвёртого метода нету, так как мы ещё не объявили структуру снарядов.
Давайте же сейчас поговорим о ней.
Структура 'Снаряды'(ShotState)
Эта структура должна описывать движение снарядов и 'забывать' (закрашивать) путь снаряда.
Каждый снаряд должен иметь направление, начальную позицию и урон.
Т.е. её поля будут следующие:
private Position Shot_position { get; set; }
private int dir { get; set; }
private int ID_Player { get; set; }
private int damage { get; set; }
private List<int> x_way { get; set; }
private List<int> y_way { get; set; }
Экземпляр класса позиции — это текущая позиция снаряда, dir — это направление движения снаряда, ID_Player — ID игрока пустившего снаряд, damage — урон этого снаряда, x_way — движение по Х, y_way — движение снаряда по Y.
Вот все методы и поля (описание их ниже)
/// <summary>
/// Забываем путь(закрашиваем его)
/// </summary>
/// <param name="shot">Снаряд</param>
public static void ForgetTheWay(ShotState shot)
{
int[] x = ShotState.x_way_array(shot);
int[] y = ShotState.y_way_array(shot);
switch (shot.dir)
{
case 0: {
for (int i = 0; i < x.Length - 1; i++)
{
Console.CursorTop = y[0];
Console.CursorLeft = x[i];
Console.Write("0");
}
} break;
case 90: {
for (int i = 0; i < y.Length - 1; i++)
{
Console.CursorLeft = x[0];
Console.CursorTop = y[i];
Console.Write("0");
}
} break;
case 180: {
for (int i = 0; i < x.Length - 1; i++)
{
Console.CursorLeft = x[i];
Console.CursorTop = y[0];
Console.Write("0");
}
} break;
case 270: {
for (int i = 0; i < y.Length - 1; i++)
{
Console.CursorTop = y[i];
Console.CursorLeft = x[0];
Console.Write("0");
}
} break;
}
}
/// <summary>
/// Конструктор снарядов
/// </summary>
/// <param name="positionShot">Позиция выстрела</param>
/// <param name="dir_">Куда летим</param>
/// <param name="ID_Player">От кого летим</param>
/// <param name="dam">Какой урон</param>
public ShotState(Position positionShot, int dir_, int ID_Player_, int dam)
{
Shot_position = positionShot;
dir = dir_;
ID_Player = ID_Player_;
damage = dam;
x_way = new List<int>(); y_way = new List<int>();
x_way.Add(Shot_position.X); y_way.Add(Shot_position.Y);
}
public static string To_string(ShotState shot)
{
return shot.ID_Player.ToString() + ":" + shot.Shot_position.X + ":"
+ shot.Shot_position.Y + ":" + shot.dir + ":" + shot.damage;
}
private Position Shot_position { get; set; }
private int dir { get; set; }
private int ID_Player { get; set; }
private int damage { get; set; }
private List<int> x_way { get; set; }
private List<int> y_way { get; set; }
private static int[] x_way_array(ShotState shot)
{
return shot.x_way.ToArray();
}
private static int[] y_way_array(ShotState shot)
{
return shot.y_way.ToArray();
}
public static void NewPosition(ShotState shot, int X, int Y)
{
shot.Shot_position.X = X;
shot.Shot_position.Y = Y;
shot.x_way.Add(shot.Shot_position.X); shot.y_way.Add(shot.Shot_position.Y);
}
public static void WriteShot(ShotState shot)
{
Console.CursorLeft = shot.Shot_position.X;
Console.CursorTop = shot.Shot_position.Y;
Console.Write("0");
}
public static void Position_plus_plus(ShotState shot)
{
switch (shot.dir)
{
case 0: { shot.Shot_position.X += 1; } break;
case 90: { shot.Shot_position.Y += 1; } break;
case 180: { shot.Shot_position.X -= 1; } break;
case 270: { shot.Shot_position.Y -= 1; } break;
}
Console.ForegroundColor = ConsoleColor.White;
Console.CursorLeft = shot.Shot_position.X; Console.CursorTop = shot.Shot_position.Y;
Console.Write("0");
shot.x_way.Add(shot.Shot_position.X); shot.y_way.Add(shot.Shot_position.Y);
}
public static Position ReturnShotPosition(ShotState shot)
{
return shot.Shot_position;
}
public static int ReturnDamage(ShotState shot)
{
return shot.damage;
}
Первый метод — мы забываем путь в консоли (т.е. закрашиваем его), через коллекции с этим путём.
Следующий — это конструктор (то есть основной метод, который настраивает наш экземпляр структуры).
Третий метод — выводит всю информацию в текстовом виде и используется только при отправке
на сервер.
Дальнейшие методы печатают/возвращают некоторые поля для дальнейшего использования.
Структура 'Стена (WallState)'
Все поля этой структуры а так же методы для представления стены и нанесения ей урона.
Вот её поля и методы:
private Position Wall_block { get; set; }
private int HP { get; set; }
private static void hp_minus(WallState wall ,int damage)
{
wall.HP -= damage;
}
/// <summary>
/// Создаём блок стены
/// </summary>
/// <param name="bloc">Координаты блока</param>
/// <param name="hp">Здоровье</param>
public WallState(Position bloc, int hp)
{
Wall_block = bloc; HP = hp;
}
public static bool Return_hit_or_not(Position pos, int damage)
{
if (pos.X <= 0 || pos.Y <= 0 || pos.X >= Width || pos.Y >= Height) { return true; }
//
//
//
for (int i = 0; i < Walls.Count; i++)
{
if ((Walls[i].Wall_block.X == pos.X) &&
(Walls[i].Wall_block.Y == pos.Y))
{
WallState.hp_minus(Walls[i], damage);
if (Walls[i].HP <= 0)
{
Console.CursorLeft = pos.X; Console.CursorTop = pos.Y;
Console.ForegroundColor = ConsoleColor.Black;
Walls.RemoveAt(i);
Console.Write("0");
Console.ForegroundColor = ConsoleColor.White;
}
return true;
}
}
return false;
}
Так вот. Подведём некий итог.
Для чего нам метод 'Return_hit_or_not'? Он возвращает было ли касание какой-либо координаты, какого-либо объекта, и наносит ему урон. Метод 'CreateShot' создаёт снаряд из конструктора.
Взаимодействие структур
В нашем основном потоке существуют два параллельных потока (Task), будем отталкиваться от них.
Task tasc = new Task(() => { Event_listener(); });
Task task = new Task(() => { To_key(); });
tasc.Start(); task.Start();
Task.WaitAll(task, tasc);
Что за потоки? Первый принимает данные от сервера и обрабатывает их, а второй отправляет данные на сервер.
Так вот, нам нужно слушать сервер (то есть принимать данные от него) и обрабатывать принятые данные, а так же выполнять операции по переданным нам данным.
Любые принятые данные от сервера в нашем проекте представляют собой объект события, где аргументы (как и имя события) разделяются символом ':', то есть на выходе мы имеем такую схему: EventName:Arg1:Arg2:Arg3:...ArgN.
Так вот, существуют так же два вида событий (так как больше и не нужно) и взаимодействия с элементами структур в нашем проекте, а именно движение танка и создание+движение снаряда.
Но мы всё ещё не знаем как принимать эти данные, не то что обрабатывать, поэтому мы лезем в прекраснейший сайт (ссылка внизу статьи) и читаем про сеть и сокеты (нам нужен именно UDP), берём их код и переделываем под себя (не забываем что нужно именно вникнуть в информацию на этом сайте, а не бездумно копировать), на выходе получается вот такой код:
static void Event_listener()
{
// Создаем UdpClient для чтения входящих данных
UdpClient receivingUdpClient = new UdpClient(localPort);
IPEndPoint RemoteIpEndPoint = null;
try
{
/*th - переменная отвечающая за сброс цикла (дабы завершить задачи и закрыть приложение)*/
while (th)
{
// Ожидание дейтаграммы
byte[] receiveBytes = receivingUdpClient.Receive(ref RemoteIpEndPoint);
// Преобразуем данные
string returnData = Encoding.UTF8.GetString(receiveBytes);
//TYPEEVENT:ARG // это наш формать приходящих данных
string[] data = returnData.Split(':').ToArray<string>(); // об этом ниже
Task t = new Task(() =>{ Event_work(data); }); t.Start(); // так же как и об этом
}
}
catch (Exception ex)
{
Console.Clear();
Console.WriteLine("Возникло исключение: " + ex.ToString() + "\n " + ex.Message);
th = false;//сбрасываем циклы приёма-передачи данных на сервер
}
}
Тут мы видим прекрасно готовый код, который выполняет ровно то о чём мы говорили выше, то есть методом '.Split(:)' мы делим текст на массив строк, далее методом '.ToArray()' собираем этот массив в переменную 'data', после чего мы создаём новый поток (асинхронный, т.е. выполняется независимо от выполнения задачи в этом метода) так же как и методе «Main», описываем его и запускаем (методом '.Start()').
Небольшое пояснение в виде картинки с кодом (я использовала для теста этой задумки этот код), этот код не относится к проекту, он просто создан для теста того кода (как аналогичный) и решения одной очень важной задачи: «Возможно ли выполнять действия независимо от кода в основном метода». Спойлер: да!
static void Main(string[] args)
{
//int id = 0;
//Task tasc = new Task(() => { SetBrightness(); });
//Task task = new Task(() => { SetBrightness(); });
//tasc.Start(); //task.Start();
//Task.WaitAll(task, tasc);
for (int i = 0; i < 5; i++)
{
Task tasc = new Task(() => { SetBrightness(); });
tasc.Start();
//Thread.Sleep(5);
}
Console.WriteLine("It's end");
Console.Read();
}
public static void SetBrightness()
{
for (int i = 0; i < 7; i++)
{
int id = i;
switch (id)
{
case 1: { Console.ForegroundColor = ConsoleColor.White; } break;
case 2: { Console.ForegroundColor = ConsoleColor.Yellow; } break;
case 3: { Console.ForegroundColor = ConsoleColor.Cyan; } break;
case 4: { Console.ForegroundColor = ConsoleColor.Magenta; } break;
case 5: { Console.ForegroundColor = ConsoleColor.Green; } break;
case 6: { Console.ForegroundColor = ConsoleColor.Blue; } break;
}
Console.WriteLine("ТЕСТ");
}
}
И вот его работа:
Четыре запущенных потока (один из которых почти выполнил свою работу):
Перемещаемся дальше, а точнее в метод выполняемый потоком:
static void Event_work(string[] Event)
{
// принимаем мы массив EventType в нём будет первым элементом
// остальные элементы (а именно те что ниже) идентичны для каждого события
// НО если событие выстрел, то добавляется урон (шестой элемент массива)
int ID = int.Parse(Event[1]),
X = int.Parse(Event[2]),
Y = int.Parse(Event[3]),
DIR = int.Parse(Event[4]);
switch (Event[0])
{
case "movetank":
{
Print_tanks(ID, X, Y, DIR);
}
break;
case "createshot":
{
ShotState shot = new ShotState(new Position(X, Y), DIR, ID, int.Parse(Event[4]));
MoveShot(shot);
}
break;
default: { return; } break;
}
}
Теперь начинает вырисовываться схема описания, если наш тип события 'movetank' то взаимодействуют только следующие элементы: 'Стены' и 'Танк'.
Но если тип события 'createshot', то взаимодействует, буквально, всё.
Если выстрел коснулся стены — то он отнял ей здоровье, если выстрел коснулся игрока — то он отнял ему здоровье, если выстрел просто улетел — то он пропал и очистился.
Если же у нас прочее событие — то мы выходим из этого метода, всё кажется простым.
Но не всё так просто, самый сок начинается если мы копаем глубже, а точнее, в вызываемые методы.
Из названия этих методов понятно, что первый — это движение танка, то есть это отрисовка и перемещение коллайдеров, а второй — создание и запуск выстрела.
Метод рисующий танки:
static void Print_tanks(int id, int x, int y, int dir)
{
PlayerState player = Players[id];
Console.ForegroundColor = ConsoleColor.Black;
PlayerState.WriteToLastPosition(player, "000\n000\n000");
/*
000
000
000
*/
switch (id)
{
case 0: { Console.ForegroundColor = ConsoleColor.White; } break;
case 1: { Console.ForegroundColor = ConsoleColor.Yellow; } break;
case 2: { Console.ForegroundColor = ConsoleColor.Cyan; } break;
case 3: { Console.ForegroundColor = ConsoleColor.Magenta; } break;
case 4: { Console.ForegroundColor = ConsoleColor.Green; } break;
case 5: { Console.ForegroundColor = ConsoleColor.Blue; } break;
}
PlayerState.NewPosition(player, x, y);
switch (dir)
{
case 270:
case 90: { Console.CursorLeft = x; Console.CursorTop = y; Console.Write("0 0\n000\n0 0"); } break;
/*
0 0
000
0 0
*/
case 180:
case 0: { Console.CursorLeft = x; Console.CursorTop = y; Console.Write("000\n 0 \n000"); } break;
/*
000
0
000
*/
}
}
И последний метод (для снаряда (создаёт и двигает его)):
private static void MoveShot(ShotState shot)
{
ShotState Shot = shot;
while ((!PlayerState.Return_hit_or_not(ShotState.ReturnShotPosition(Shot), ShotState.ReturnDamage(Shot))) &&
(!WallState.Return_hit_or_not(ShotState.ReturnShotPosition(Shot), ShotState.ReturnDamage(Shot))))
{
// если достигли координат стен - то вернёт фалс и будет брейк
ShotState.Position_plus_plus(Shot);
}
Console.ForegroundColor = ConsoleColor.Black;//забываем путь полёта пули(закрашиваем его)
ShotState.ForgetTheWay(Shot);
}
Вот это и весь наш приём и обработка событий, теперь перейдём к их созданию (методу создателю и отправщику их на сервер)
Создаём события (To_key())
Вот целый метод создающий события, меняющий наши координаты и отправляющий всё это на сервер (описание ниже):
static void To_key()
{
//Приём нажатой клавиши
PlayerState MyTank = Players[MY_ID];
System.Threading.Timer time = new System.Threading.Timer(new TimerCallback(from_to_key), null, 0, 10);
while (true)
{
Console.CursorTop = 90; Console.CursorLeft = 90;
switch (Console.ReadKey().Key)
{
case ConsoleKey.Escape:
{ time.Dispose(); th = false; break; }
break;
//пробел
case ConsoleKey.Spacebar:
{
if (for_shot)
{
//"createshot"
var shot = PlayerState.CreateShot(Players[MY_ID], PlayerState.NewPosition_X(MyTank, '\0'), 3);
MessageToServer("createshot:" + PlayerState.To_string(MyTank) + ":3");// дамаг - 3
var thr = new Task(() => { MoveShot(shot); });
for_key = false;//откат кнопок
for_shot = false;//откат выстрела
}
}
break;
case ConsoleKey.LeftArrow:
{
if (for_key)
{
PlayerState.NewPosition_X(MyTank, '-');
MessageToServer("movetank:" + PlayerState.To_string(MyTank)); for_key = false;
}
}
break;
case ConsoleKey.UpArrow:
{
if (for_key)
{
PlayerState.NewPosition_Y(MyTank, '-');
MessageToServer("movetank:" + PlayerState.To_string(MyTank)); for_key = false;
}
}
break;
case ConsoleKey.RightArrow:
{
if (for_key)
{
PlayerState.NewPosition_X(MyTank, '+');
MessageToServer("movetank:" + PlayerState.To_string(MyTank)); for_key = false;
}
}
break;
case ConsoleKey.DownArrow:
{
if (for_key)
{
PlayerState.NewPosition_Y(MyTank, '+');
MessageToServer("movetank:" + PlayerState.To_string(MyTank)); for_key = false;
}
}
break;
case ConsoleKey.PrintScreen:
{ }
break;
case ConsoleKey.A:
{
if (for_key)
{
PlayerState.NewPosition_X(MyTank, '-');
MessageToServer("movetank:" + PlayerState.To_string(MyTank)); for_key = false;
}
}
break;
case ConsoleKey.D:
{
if (for_key)
{
PlayerState.NewPosition_X(MyTank, '+');
MessageToServer("movetank:" + PlayerState.To_string(MyTank)); for_key = false;
}
}
break;
// Аналог нажатия на пробел
case ConsoleKey.E:
{
if (for_shot)
{
for_key = false;
for_shot = false;
}
}
break;
// Аналог нажатия на пробел, но спец выстрел
case ConsoleKey.Q:
break;
case ConsoleKey.S:
{
if (for_key)
{
PlayerState.NewPosition_Y(MyTank, '+');
MessageToServer("movetank:" + PlayerState.To_string(MyTank)); for_key = false;
}
}
break;
case ConsoleKey.W:
{
if (for_key)
{
PlayerState.NewPosition_Y(MyTank, '-');
MessageToServer("movetank:" + PlayerState.To_string(MyTank)); for_key = false;
}
}
break;
case ConsoleKey.NumPad2:
{
if (for_key)
{
PlayerState.NewPosition_Y(MyTank, '+');
MessageToServer("movetank:" + PlayerState.To_string(MyTank)); for_key = false;
}
}
break;
case ConsoleKey.NumPad4:
{
if (for_key)
{
PlayerState.NewPosition_X(MyTank, '-');
MessageToServer(PlayerState.To_string(MyTank));
}
}
break;
case ConsoleKey.NumPad6:
{
if (for_key)
{
PlayerState.NewPosition_X(MyTank, '+');
MessageToServer("movetank:" + PlayerState.To_string(MyTank)); for_key = false;
}
}
break;
//нажатие на пробел
case ConsoleKey.NumPad7:
{
if (for_shot)
{
for_key = false;
for_shot = false;
}
}
break;
case ConsoleKey.NumPad8:
{
if (for_key)
{
PlayerState.NewPosition_Y(MyTank, '-');
MessageToServer("movetank:" + PlayerState.To_string(MyTank)); for_key = false;
}
}
break;
// Аналог нажатия на пробел но спец выстрел
case ConsoleKey.NumPad9:
break;
default:
break;
}
}
}
Тут мы используем один и тот же метод 'MessageToServer', его цель — отправить данные на сервер.
И методы 'NewPosition_Y' и 'NewPosition_X', которые назначают нашему танку новую позицию.
(в кейсах — используемые клавиши, я пользуюсь в основном стрелками и пробелом — вы можете выбрать свой вариант и скопипастить код из кейса '.Spase' в лучший для вас вариант (или же написать его (указать клавишу) самому))
И вот последний метод из взаимодействия событий клиента-сервера, сама отправка на сервер:
static void MessageToServer(string data)
{
/* Тут будет отправка сообщения на сервер */
// Создаем UdpClient
UdpClient sender = new UdpClient();
// Создаем endPoint по информации об удаленном хосте
IPEndPoint endPoint = new IPEndPoint(remoteIPAddress, remotePort);
try
{
// Преобразуем данные в массив байтов
byte[] bytes = Encoding.UTF8.GetBytes(data);
// Отправляем данные
sender.Send(bytes, bytes.Length, endPoint);
}
catch (Exception ex)
{
Console.WriteLine("Возникло исключение: " + ex.ToString() + "\n " + ex.Message);
th = false;
}
finally
{
// Закрыть соединение
sender.Close();
}
}
Теперь самое главное — перезарядка движения и выстрела (перезарядка движения как античит, и небольшое время простоя для обработки на других машинах).
Этим занимается таймер в методе 'To_key()', а точнее 'System.Threading.Timer time = new System.Threading.Timer(from_to_key(), null, 0, 10);'.
В этой строчке кода мы создаём новый таймер, назначаем ему метод для управления ('from_to_key()'), указываем что не передаём туда ничего 'null', время с которого начнётся счёт таймера '0'( ноль миллисекунд (1000ms(миллисекунда) — 1s (секунда)) и интервал вызова метода (в миллисекундах) '10' (кстати метод 'To_key()' полностью настроен на перезарядку (это выражается в условиях в кейсах, они связанны с полями в классе Program)).
Выглядит этот метод так:
private static void from_to_key(object ob)
{
for_key = true;
cooldown--;
if (cooldown <= 0) { for_shot = true; cooldown = 10; }
}
Где 'cooldown' — это перезарядка (выстрела).
И всё же большинство элементов в этом проекте — это поля:
private static IPAddress remoteIPAddress;// ай пи
private static int remotePort;//порт
private static int localPort = 1011;//локальный порт
static List<PlayerState> Players = new List<PlayerState>();// игроки
static List<WallState> Walls = new List<WallState>();//
//--------------------------------
static string host = "localhost";
//--------------------------------
/* Тут должно быть получение координат с сервера и назначение их нашему танчику */
static int Width;/* Высота и ширина игрового поля */
static int Height;
static bool for_key = false;
static bool for_shot = false;
static int cooldown = 10;
static int MY_ID = 0;
static bool th = true;//для завершения потока
Наконец конец
Вот и конец данной статьи с описанием кода проекта танчиков, мы смогли реализовать почти всё, кроме одного — загрузки данных с сервера (стены, танки(игроки)) и назначения нашему танчику начальных координат. Этим мы займёмся в следующей статье, где будем уже касаться сервера.
Ссылки, упомянутые в статье:
Мой репозиторий
Классные программисты, который мне помогли написать этот код и научили новому: Habra-Mikhail, norver.
Спасибо за прочтение данной статьи, если я в чём-то на ваш взгляд оказалась не права — пишите в комментарии и мы вместе это изменим.
Так же прошу обращать внимание на комментарии, так как в них обсуждаются улучшения как проекта, так и кода. Если захотите помочь в переводе книги — прошу написать мне в сообщения или на почту: koito_tyan@mail.ru.
Да начнётся игра!
Комментарии (36)
planarik
14.12.2017 15:04И это работает?
private static TimerCallback from_to_key() { for_key = true; cooldown--; if (cooldown <= 0) { for_shot = true; cooldown = 10; } throw new NotImplementedException(); }
koito_tyan Автор
14.12.2017 15:05Не уверенна, сейчас проверю)
planarik
14.12.2017 15:11Да, интересно. Исключение из callback таймера, оказывается, не возвращается. Правда, остается открытым вопрос — а зачем оно в методе?
koito_tyan Автор
14.12.2017 15:12Убрала и подправила, спасибо за указание на ошибку)
ser-mk
14.12.2017 16:07Хех, успешного code review на Хабре
ну а так...не тортkoito_tyan Автор
14.12.2017 16:55Опишите пожалуйста что вам не понравилось (я хочу написать следующую статью, и по возможности исправить эту, в более лучшей версии)
koito_tyan Автор
14.12.2017 17:02понятную и красочную* (заместо лучшей)
(истекло время редактирования)
Vest
14.12.2017 16:43Я тут решил прогнать ваш проект бесплатной PVS-Studio. Я взял ветку final-v1.3, до 4го коммита. И хотел бы обратить ваше внимание на предупреждения, что она мне показала:
V3010 — The return value of function 'CreateShot' is required to be utilized. Program.cs 241PlayerState.CreateShot(Players[MY_ID], PlayerState.NewPosition_X(MyTank, '\0'), 3);
Ваш метод создаёт объект, возвращает его, но не использует.
И два предупреждения:
V3102 — Suspicious access to element of 'x' object by a constant index inside a loop. Program.cs 551, 559
В обоих случаях вы итерируете по «x», но индекс используете для «y».for (int i = 0; i < x.Length - 1; i++) { Console.CursorLeft = x[0]; Console.CursorTop = y[i];
Это не реклама продукту, я просто мимо проходил. Поправьте или проверьте у себя, пожалуйста, заранее, чтобы потом не выстрелило.
p.s. ах да, строчки могут отличаться на 2, потому что для PVS-Studio пришлось добавить 2 строки комментария.
Andrey2008, надеюсь я вашу лицензию не нарушил. Я временно добавил шапку и убрал её.koito_tyan Автор
14.12.2017 16:51Всё хорошо, лицензию не завезли (не переживайте)
Спасибо за указание на ошибку, изменила строчки кода и сейчас обновлю код в рипозиторие )
FadeToBlack
14.12.2017 21:02Вот мне не ясно: зачем сразу статью писать? Можно же просто складывать их в папочку и никогда не публиковать, как это делал я, когда только научившись чему-нибудь брался писать статьи. Почему бы не дождаться, когда ваш код станет простым и ясным, чтобы быть хорошим примером, а не вот эти тысячи строк.
koito_tyan Автор
14.12.2017 22:06Хорошо, но как я пойму простой код или нет, как можно оценить свой код не слушая критику?
Самооценивание и сравнение? Но эти методы почти не работают с новичками (и я тому, как видите пример (хотя все остальные проекты живут в ЯндексДиск (в папочке)).FadeToBlack
15.12.2017 06:361. Освоить создание своих типов с перегрузкой операторов, чтобы не писать по две строчки одинакового кода
2. Освоить функцию cos / sin (или написать функцию angle to dir), чтобы не писать switchcase для 4х случаев.koito_tyan Автор
16.12.2017 12:03Но смысл от косинуса или синуса, если мы используем всего 4 случая?
Это не WinForm версия…FadeToBlack
17.12.2017 07:49угол определяет направление через cos/sin. лучше абстракции (и меньшее количество кода) сложно придумать.
mayorovp
17.12.2017 08:59Потому что вы сначала отказались от направления через перечисление в пользу угла под предлогом упрощения перехода к WinForm версии — а теперь этот переход усложняете.
FadeToBlack
15.12.2017 06:423. Понять, что булевы выражения уже годятся, чтобы возвращать результат, а дополнительный код не требуется
if (startPosition.X > 0 && startPosition.Y > 0) return true; return false;
FadeToBlack
15.12.2017 06:484. Слишком много static. Для этого существуют синглтоны, в таком случае очень легко конвертить синглтон в обычный объект (когда потребуется сделать множество контекстов абстракции и так далее).
FadeToBlack
15.12.2017 08:21Жду еще пару статей на тему «настало время еще раз все переделать».
koito_tyan Автор
16.12.2017 12:02Их будет много, пока не напишу сервер и не протестирую в полевых условиях )
DarkerSigner
14.12.2017 22:04Буду конечно занудой, но я думаю вам стоит все же разбить код на несколько файлов, а то получается солянка и банально не удобно читать.
koito_tyan Автор
14.12.2017 22:07Да, вы правы, как только я допишу ещё два метода (на загрузку) я сделаю нормально распределение (смотрите в рипозитории изменения). Спасибо за ваше 'занудство', ведь оно помогает и мне, и остальным )
ForNeVeR
15.12.2017 07:46Это хорошая и правильная идея — предоставлять свой код для оценки сообществом (даже если вы новичок, в этом нет ничего зазорного). Другой разговор, что сообщество Хабра, возможно, для этого не лучшим образом подходит (на этом ресурсе люди всё-таки чаще ожидают каких-то законченных материалов для специалистов среднего и высокого уровня). Мне кажется, что куда больше фидбэка вы сможете получить, если обратитесь в места с более высокой концентрацией .NET-разработчиков.
Вот тут перечислено несколько .NET-сообществ в Telegram, приходите :)
koito_tyan Автор
16.12.2017 12:01Обязательно загляну к вам как только будет свободное время (немного много учёбы) )
mayorovp
15.12.2017 13:50Зачем вы вообще проверяете направление выстрела в методе ForgetTheWay, если все прошлые позиции уже записаны в списках x_way и y_way?
koito_tyan Автор
16.12.2017 12:01Я думала что так будет понятнее что удалять, протестирую другие варианты и изменю, спасибо за поправку )
mayorovp
15.12.2017 13:57Кстати, вы совершенно напрасно выбрали UDP-сокеты. Эти сокеты подходят только если вы делаете свой протокол с контролем потерь пакетов (совершенно не ваш случай) или же если несколько потерянных пакетов ничего не изменят.
У вас потеря пакетов с выстрелами порушит всю игру. Не надо так делать, используйте TCP.koito_tyan Автор
16.12.2017 11:51Я придумала идею получше и уже обсуждаю её, если хотите можете присоединится к нам, вот мой vk: vk.com/unicode_72
mayorovp
15.12.2017 14:01По поводу многопоточности: что-то я не вижу у вас в коде никаких блокировок. А они нужны раз уж у вас доступ к одним и тем же объектам идет сразу из кучи потоков!
Как минимум, надо отказаться от обработки каждого пришедшего события в новой задаче — в этом нет смысла; обрабатывать их все можно просто подряд.
А дальше надо думать как синхронизировать «клавиатурный поток» с сетевым. Или наставить локов на каждую операцию, или складывать все события в блокирующую очередь, которую будет разбирать третий поток.koito_tyan Автор
16.12.2017 12:00Спасибо за поправку, вы помогли сделать мой код лучше, допилю код и выложу в рипозиторий )
azhira
16.12.2017 00:50Вам уже много замечаний высказали по содержанию, а от меня будет капелька занудства по оформлению.
- Среди большинства .NET разработчиков принят CamelCase стиль именования классов/методов. Если он Вам не мил, используйте другой, но придерживайтесь его везде в рамках одного проекта. Сейчас же можно встретить идущие подряд методы Event_work, MoveShot и from_to_key.
- По возможности выбирайте названия переменных так, чтобы их назначение было понятным без комментариев. Вот так точно не надо (я сначала подумала, что tasc — это опечатка):
Task tasc = new Task(() => { Event_listener(); }); Task task = new Task(() => { To_key(); });
- Если же комментируете код, то пишите, почему добавлена та или иная строка, а не что она делает. И не забывайте актуализировать:
// Создаем экземпляр класса Position, с координатами 2, 2 var startPosition = new Position(10, 5);
- Про разбиение кода на несколько файлов уже говорили. Можно было хотя бы #region использовать, чтобы структурировать.
Я понимаю, сейчас это все выглядит несущественным, но нужно прививать себе хорошие привычки.
Извините, если высказалась резковато. Вы смелая и, надеюсь, упорная. Интересно посмотреть, что получится в итоге.koito_tyan Автор
16.12.2017 11:58Всё хорошо. Я люблю критику )
А по поводу кода… я изменю его как только будет готово взаимодействие с сервером перед началом игры (уже пишу третью функцию), все изменения есть в рипозитории )
zCooler
Репозитории то пустые :)
koito_tyan Автор
Немного сглупила, поправила ссылку.