Tanchiki in the console, article two: "It is time to redo everything!"

And still a game!

Hello again everyone! I'm glad that you are reading this, because our story about the dispute is approaching the final stage.

In a previous article, I made code sketches and a few days later (thanks to the advice of more experienced programmers) I’m ready to show you a completely rewritten code from scratch with explanations.

You can download the finished code at the end of the article from my ripository (if you can’t wait).

Let's start from the beginning, we will analyze the initial objects

Here we will analyze what will happen in our application and what we will wind up with tape on it (of course, code).

1st, of course, Walls
2nd, these are our Players (
3rd ) , shells (Shots)

The question arises, but how can this be systematized and made to work together?

What is able to create a system? Of course, a structure, but any structure should have parameters. Now we will create the first of them - these are the coordinates. For their convenient display, we will use the following class:

`````` // Класс для удобного представления координат
public class Position
{
// Публичные свойства класса
public int X { get; set; }
public int Y { get; set; }
public Position(int x, int y)
{
X = x;
Y = y;
}
}
``````

The second parameter is the distinguishing number, because each element of the structure must be different, so in any of our structure (inside it) there will be an ID field.

Let's start creating structures based on this class.
Next, I will describe the structures and then their interaction.

Players structure (PlayerState)

I singled them out separately, because they have a huge number of methods and they are very important (who else will play and move the models?).

I’ll just drop the fields and start describing them below:

`````` 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 - I already managed to explain
Position - this is an instance of the class of the same name, LastPosition - the previous position
Collider - this is the collider (those points that hit your health)

The structure 'Players' should contain a method for processing instances of the structure / preparing our instance for sending to the server, for these tasks we use the following methods:

``````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)
``````

The first method is a method for taking a certain amount of health from a player.
The second method is necessary to assign a new position to the players.

We use the third in the constructor so that there are no errors when creating the tank.
The fourth fires a shot (i.e., fires a bullet from a tank).

The fifth should print the text to the previous position of the player (do not update us every frame the console screen by calling Console.Clear ()).

Now, for each method separately, that is, we will analyze their code:

1st:

``````   ///
/// Минус хп
///
/// Игрок
/// Сколько хп отнимаем
public static void hp_minus(PlayerState player, int hp)
{
player.hp -= hp;
}
``````

I think that there’s not much to explain here, this operator record is completely equivalent to this one:

`` player.hp = player.hp - hp;``

The rest in this method will not be added.

2nd:

``````   ///
/// Назначаем другим игрокам позиции
///
/// Игрок
/// Координата X
/// Координата Y
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;
}
}
``````

Here we combine the conditions so that another player (on our console, suddenly there will be lags) could not leave the playing field. By the way, the fields we use (Height and Width) they denote the boundaries of our field (Height and Width).

3rd:

`````` private static bool ForExeption(Position startPosition)
{
if (startPosition.X > 0 && startPosition.Y > 0) return true;
return false;
}
``````

Here we do not allow the coordinates to be smaller than the size of the playing field.

By the way: in the console, the coordinate system starts from the upper left corner (point 0; 0) and there is a restriction (which can also be set) to 80 by x and 80 by y. If we reach 80 by x, then we crash (that is, the application breaks), and if 80 by y, then the size of the field simply increases (you can configure these restrictions by clicking on the console and selecting properties).

5th:

``````public static void WriteToLastPosition(PlayerState player, string TEXT)
{
Console.CursorLeft = player.LastPosition.X; Console.CursorTop = player.LastPosition.Y;
Console.Write(TEXT);
}
``````

Here we just print the text to the previous position (paint over it).

There is no fourth method, since we have not yet announced the structure of the shells.

Shells Structure (ShotState)

This structure should describe the movement of shells and 'forget' (paint over) the path of the shell.

Each projectile must have a direction, an initial position and damage.

Those. its fields will be as follows:

`````` private Position Shot_position { get; set; }
private int dir { get; set; }
private int ID_Player { get; set; }
private int damage { get; set; }
private List x_way { get; set; }
private List y_way { get; set; }
``````

The instance of the position class is the current position of the projectile, dir is the direction of the projectile movement, ID_Player is the ID of the player who launched the projectile, damage is the damage of this projectile, x_way is the movement along X, y_way is the movement of the projectile along Y.

Here are all the methods and fields (description of them below)

``````///
/// Забываем путь(закрашиваем его)
///
/// Снаряд
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;
}
}
///
/// Конструктор снарядов
///
/// Позиция выстрела
/// Куда летим
/// От кого летим
/// Какой урон
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(); y_way = new List();
}
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 x_way { get; set; }
private List 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;
}
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");
}
public static Position ReturnShotPosition(ShotState shot)
{
return shot.Shot_position;
}
public static int ReturnDamage(ShotState shot)
{
return shot.damage;
}
``````

The first method - we forget the path in the console (i.e. paint over it), through collections with this path.

The next is the constructor (that is, the main method that configures our instance of the structure).

The third method - displays all the information in text form and is used only when sending
to the server.

Further methods print / return some fields for future use.

Structure 'Wall (WallState)'

All fields of this structure as well as methods for representing the wall and causing damage to it.

Here are its fields and methods:

``````private Position Wall_block { get; set; }
private int HP { get; set; }
private static void hp_minus(WallState wall ,int damage)
{
wall.HP -= damage;
}
///
/// Создаём блок стены
///
/// Координаты блока
/// Здоровье
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;
}
``````

So here. To summarize.

Why do we need the 'Return_hit_or_not' method? It returns whether there was a touch of any coordinate, any object, and inflicts damage on it. The 'CreateShot' method creates a shell from the constructor.

Structural Interaction

``````Task tasc = new Task(() => { Event_listener(); });
``````

What kind of flows? The first receives data from the server and processes them, and the second sends data to the server.

So, we need to listen to the server (that is, receive data from it) and process the received data, as well as perform operations on the data transmitted to us.

Any received data from the server in our project is an event object where the arguments (like the name of the event) are separated by the ':' symbol, that is, at the output we have the following scheme: EventName: Arg1: Arg2: Arg3: ... ArgN.

So, there are also two types of events (since they are no longer needed) and interactions with structural elements in our project, namely tank movement and creation + projectile movement.

But we still don’t know how to receive this data, not what to process, so we climb into the most beautiful site (link at the bottom of the article) and read about the network and sockets (we need UDP), take their code and redo it for ourselves (do not forget what you need to delve into the information on this site, rather than copying thoughtlessly), the output is this code:

`````` static void Event_listener()
{
// Создаем UdpClient для чтения входящих данных
UdpClient receivingUdpClient = new UdpClient(localPort);
IPEndPoint RemoteIpEndPoint = null;
try
{
/*th - переменная отвечающая за сброс цикла (дабы завершить задачи и закрыть приложение)*/
while (th)
{
// Ожидание дейтаграммы
// Преобразуем данные
//TYPEEVENT:ARG // это наш формать приходящих данных
string[] data = returnData.Split(':').ToArray(); // об этом ниже
Task t = new Task(() =>{ Event_work(data); }); t.Start(); // так же как и об этом
}
}
catch (Exception ex)
{
Console.Clear();
Console.WriteLine("Возникло исключение: " + ex.ToString() + "\n  " + ex.Message);
th = false;//сбрасываем циклы приёма-передачи данных на сервер
}
}
``````

Here we see perfectly prepared code that executes exactly what we said above, that is, with the '.Split (:)' method, we divide the text into an array of strings, then with the '.ToArray ()' method we collect this array into the 'data' variable , after which we create a new thread (asynchronous, that is, it is executed regardless of the task execution in this method) as well as the “Main” method, describe it and start it (using the '.Start ()' method).

A small explanation in the form of a picture with a code (I used this code to test this idea), this code is not related to the project, it was just created to test that code (as similar) and solve one very important task: “Is it possible to perform actions regardless the code is basically a method. " Spoiler: yes!

`````` static void Main(string[] args)
{
//int id = 0;
for (int i = 0; i < 5; i++)
{
tasc.Start();
}
Console.WriteLine("It's end");
}
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("ТЕСТ");
}
}
``````

And here is his work:

Four running threads (one of which almost completed its work): We

move further, or rather, to the method executed by the thread:

`````` 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;
}
}
``````

Now the description scheme begins to emerge, if our type of event is 'movetank' then only the following elements interact: 'Walls' and 'Tank'.

But if the event type is 'createshot', then everything literally interacts.

If the shot touched the wall - then it took her health, if the shot touched the player - then he took his health, if the shot just flew away - then it disappeared and cleared.

If we have another event, then we exit this method, everything seems simple.
But not everything is so simple, the juice itself begins if we dig deeper, and more precisely, into the called methods.

From the name of these methods it is clear that the first is the movement of the tank, that is, it is drawing and moving colliders, and the second is the creation and launch of a shot.

The method of drawing tanks:

``````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
*/
}
}
``````

And the last method (for the projectile (creates and moves it)):

``````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);
}
``````

This is all our reception and processing of events, now let's move on to their creation (the method for creating and sending them to the server)

We create events (To_key ())

Here is a whole method that creates events, changes our coordinates and sends it all to the server (description below):

``````static void To_key()
{
//Приём нажатой клавиши
PlayerState MyTank = Players[MY_ID];
while (true)
{
Console.CursorTop = 90; Console.CursorLeft = 90;
{
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;
{
if (for_key)
{
PlayerState.NewPosition_Y(MyTank, '+');
MessageToServer("movetank:" + PlayerState.To_string(MyTank)); for_key = false;
}
}
break;
{
if (for_key)
{
PlayerState.NewPosition_X(MyTank, '-');
MessageToServer(PlayerState.To_string(MyTank));
}
}
break;
{
if (for_key)
{
PlayerState.NewPosition_X(MyTank, '+');
MessageToServer("movetank:" + PlayerState.To_string(MyTank)); for_key = false;
}
}
break;
//нажатие на пробел
{
if (for_shot)
{
for_key = false;
for_shot = false;
}
}
break;
{
if (for_key)
{
PlayerState.NewPosition_Y(MyTank, '-');
MessageToServer("movetank:" + PlayerState.To_string(MyTank)); for_key = false;
}
}
break;
// Аналог нажатия на пробел но спец выстрел
break;
default:
break;
}
}
}
``````

Here we use the same 'MessageToServer' method, its purpose is to send data to the server.

And the methods 'NewPosition_Y' and 'NewPosition_X', which assign our tank a new position.
(in the cases - the keys used, I mainly use the arrows and the spacebar - you can choose your own option and paste the code from the '.Spase' case into the best option for you (or write it (specify the key) yourself))

And here is the last method from the interaction of client-server events, sending to the server itself:

``````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();
}
}
``````

Now the most important thing is movement and shot reloading (movement reloading as an anti-cheat, and a short downtime for processing on other machines).

This is done by the timer in the 'To_key ()' method, or rather 'System.Threading.Timer time = new System.Threading.Timer (from_to_key (), null, 0, 10);'.

In this line of code, we create a new timer, assign it a control method ('from_to_key ()'), indicate that we don’t pass anything 'null' there, the time from which the count of the timer '0' starts (zero milliseconds (1000ms (milliseconds) - 1s (second)) and the method call interval (in milliseconds) is '10' (by the way, the 'To_key ()' method is fully configured to reload (this is expressed in conditions in cases, they are associated with fields in the Program class)).

This method looks like this :

``````private static void from_to_key(object ob)
{
for_key = true;
cooldown--;
if (cooldown <= 0) { for_shot = true; cooldown = 10; }
}
``````

Where 'cooldown' is a reload (shot).

And yet, most of the elements in this project are fields:

`````` private static IPAddress remoteIPAddress;// ай пи
private static int remotePort;//порт
private static int localPort = 1011;//локальный порт
static List Players = new List();// игроки
static List Walls = new List();//
//--------------------------------
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;//для завершения потока
``````

Finally the end

This is the end of this article with the description of the tank project code, we were able to implement almost everything except one - loading data from the server (walls, tanks (players)) and assigning our coordinates to our tank. We will deal with this in the next article, where we will already touch on the server.

My repository.
Cool programmers who helped me write this code and taught me something new: Habra-Mikhail , norver .

Also, please pay attention to the comments, as they discuss improvements to both the project and the code. If you want to help in translating the book - please write to me in messages or by mail: koito_tyan@mail.ru.

Let the game begin!

Only registered users can participate in the survey. Please come in.

Do you find it necessary to translate the second edition of RustBook

• 31.4% Yes, I want to see what's new 11
• 11.4% Yes, I want to join the translation team for this book 4
• 25.7% No, I’m only here for c # and I’m waiting on it for the server too! 9
• 14.2% No, no explanation 5
• 17.1% I will remain neutral and if I want - I will express my position in the comments 6