📜 ⬆️ ⬇️

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

And yet the game!


Hello everyone again! I'm glad you're reading this, because our story about the dispute comes to the final stage.

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

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

Let's start from the beginning, analyze the initial objects.


Here we will analyze what will be in our application and what we will be winding up with electrical tape to this (of course, the code).
')
1st, of course, the walls (Walls)
2nd, these are our players
3rd, shells (Shots)

The question arises, how to systematize and force it to work together?

What can create a system? Of course, the structure, but any structure must have parameters. Now we will create the first of them - these are coordinates. To display them conveniently, 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 a distinctive number, because each element of the structure must be different, therefore in any of our structure (inside it) there will necessarily be an ID field.

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

Player Structure (PlayerState)


I singled them out separately, as there is a huge amount of methods in them and they are very important (who else will play and move the models?).

I'll just take off 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 is an instance of the class of the same name, LastPosition is the previous position
Collider - this is a collider (those points, hitting which you will take away health)

The 'Players' structure, must contain methods for handling 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 the method of taking a certain amount of health from the player.
The second method is necessary to assign a new position to players.

The third one we use in the constructor so that there are no mistakes when creating the tank.
The fourth creates a shot (i.e., shoots a bullet from a tank).

The fifth one should print the text to the previous position of the player (do not refresh the console screen with each frame by calling Console.Clear ()).

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

1st:

  /// <summary> ///   /// </summary> /// <param name="player"></param> /// <param name="hp">  </param> public static void hp_minus(PlayerState player, int hp) { player.hp -= hp; } 

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

  player.hp = player.hp - hp; 

The rest of this method will not be added.

2nd:

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

Here we combine the conditions so that another player (we will have lags on the 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 borders 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 give the coordinates to be less 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 adjusted) to 80 x and 80 y. If we reach 80 in x, we are painted (that is, the application breaks), and if 80 in y, the field size simply increases (you can adjust 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 in the previous position (paint over it).

There is no fourth method, since we have not yet declared the structure of the shells.
Let's talk about her now.

Structure 'Shells' (ShotState)


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

Each projectile must have a direction, starting 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<int> x_way { get; set; } private List<int> y_way { get; set; } 

A copy of the position class is the current position of the projectile, dir is the direction of movement of the projectile, ID_Player is the player ID of the projectile, damage is the damage of this projectile, x_way is movement along X, y_way is movement of projectile along Y.

Here are all the methods and fields (described below)

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

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

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

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

Further methods print / return some fields for further 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; } /// <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; } 

So here. Let's sum up some.

Why do we need the method 'Return_hit_or_not'? It returns whether it touched any coordinate, any object, and deals damage to it. The 'CreateShot' method creates a shell from the constructor.

Interaction of structures


In our main thread there are two parallel threads (Task), we will build on them.

 Task tasc = new Task(() => { Event_listener(); }); Task task = new Task(() => { To_key(); }); tasc.Start(); task.Start(); Task.WaitAll(task, tasc); 

What are the streams? The first one takes the data from the server and processes it, and the second sends the 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 (as well as the name of the event) are separated by the ':' symbol, that is, we have the following output at the output: EventName: Arg1: Arg2: Arg3: ... ArgN.

So, there are also two types of events (since there is no need for more) and interactions with the elements of the structures in our project, namely, the movement of the tank and the creation + movement of the projectile.

But we still don’t know how to receive this data, not just what to process, so we climb into the nicest 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 (don’t forget it is necessary to penetrate the information on this site, and not to copy it thoughtlessly), the output is such code:

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

Here we see perfectly prepared code that performs exactly what we said above, that is, using the '.Split (:)' method, we divide the text into an array of strings, then use the '.ToArray () method to assemble this array into the' data 'variable , after which we create a new thread (asynchronous, ie, it is executed independently of the task execution in this method) as well as the “Main” method, we describe it and run (by 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 does not apply to the project, it was simply created to test that code (as similar) and solve one very important task: “Is it possible to perform actions regardless of code is basically a method. " Spoiler: yes!

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

And here is his job:

image

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

image

Moving further, or rather into the method performed 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 starts to emerge, if our type of event is 'movetank', then only the following elements interact: 'Walls' and 'Tank'.

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

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

If we have another event - then we leave this method, everything seems simple.
But not everything is so simple, the very juice begins if we dig deeper, or rather, 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 the drawing and moving of the colliders, and the second is the creation and launch of the shot.

Tank painting method:

 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 our whole reception and event handling, now let's move on to creating them (the method to the creator and sender of them to the server)

Create events (To_key ())


Here is a whole method of creating events, changing our coordinates and sending it all to the server (description below):

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

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

And the methods 'NewPosition_Y' and 'NewPosition_X', which assign a new position to our tank.
(in cases - used keys, I mainly use arrows and space - you can choose your own version and copy 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 reloading the movement and the shot (reloading the movement as anti-cheat, and little downtime for processing on other machines).

This is done by the timer in the 'To_key ()' method, or rather by '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 method to control ('from_to_key ()'), indicate that we don’t send anything to 'null', the time from which the count of the timer '0' will start (zero milliseconds (1000ms (millisecond) - 1s (second) and method call interval (in milliseconds) '10' (by the way, the 'To_key ()' method is fully configured to recharge (this is expressed in the conditions in the 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 recharge (shot).

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<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;//   

Finally the end


And this is the end of this article with a description of the code for the project of the tanchiki, we were able to implement almost everything except for one - downloading data from the server (walls, tanks (players)) and assigning our initial coordinates to our tank. This we will do in the next article, where we will already touch the server.

Links mentioned in the article:

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

Thank you for reading this article, if I somehow turned out to be wrong in your opinion - write in a comment and we will change it together.

I also ask you to pay attention to the comments, as they discuss the improvements of both the project and the code. If you want to help in the translation of the book - please write me a message or mail: koito_tyan@mail.ru.

Let the game begin!

Source: https://habr.com/ru/post/344686/


All Articles