Pull to refresh

Танчики в консоли, статья вторая: «Настало время всё переделать!»

Reading time15 min
Views5.1K

И всё же игра!


Всем снова привет! Рада что вы читаете это, ведь наша история о споре подходит к финальной стадии.

В прошлой статье я сделала наброски кода и спустя несколько дней (благодаря советам более опытных программистов) готова вам показать полностью переписанный с нуля код с объяснениями.

Скачать готовый код можно в конце статьи из моего рипозитория (если не можете дождаться).

Начнём с начала, разберём начальные объекты


Тут мы разберём что будет в нашем приложении и что к этому будем заматывать изолентой (конечно же кодом).

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("ТЕСТ");
    }
    }

И вот его работа:

image

Четыре запущенных потока (один из которых почти выполнил свою работу):

image

Перемещаемся дальше, а точнее в метод выполняемый потоком:

 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.

Да начнётся игра!
Only registered users can participate in poll. Log in, please.
Считаете ли вы нужным перевод второго издания книги RustBook
31.43% Да, я хочу увидеть что там нового11
11.43% Да, я хочу вступить в команду по переводу этой книги4
25.71% Нет, я тут только для c# и жду на нём и сервер!9
14.29% Нет, без объяснений5
17.14% Я останусь нейтральным и если захочу — выражу свою позицию в комментариях6
35 users voted. 19 users abstained.
Tags:
Hubs:
Total votes 13: ↑8 and ↓5+3
Comments37

Articles