コンソールのTanchiki、記事2:「すべてをやり直す時です!」

そしてまだゲームです!



みなさんこんにちは! 論争についての私たちの物語が最終段階に近づいているので、あなたがこれを読んでいてうれしいです。



前の記事でコードスケッチを作成し、数日後に(経験豊富なプログラマーのアドバイスのおかげで)完全に書き直されたコードを説明付きで一から表示する準備ができました。



完成したコードは、私のリポジトリから記事の最後にダウンロードできます(待ちきれない場合)。



最初から始めましょう、最初のオブジェクトを分析します



ここでは、アプリケーションで何が起こるか、そしてテープ(もちろんコード)で何が起こるかを分析します。



1番目、もちろん、壁(壁)

2番目に、これらは私たちのプレーヤー(プレーヤー)です。

3番目、シェル(ショット)



問題が発生しますが、それをどのように体系化し、連携させるか



システムを作成できるのは何ですか? もちろん、構造ですが、どの構造にもパラメータが必要です。 次に、最初のオブジェクトを作成します-これらは座標です。 便利な表示のために、次のクラスを使用します。



//      public class Position { //    public int X { get; set; } public int Y { get; set; } public Position(int x, int y) { X = x; Y = y; } }
      
      





2番目のパラメーターは識別番号です。これは、構造の各要素が異なる必要があるため、構造のいずれか(内部)に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-私はすでにそれを説明することができました

位置は同じ名前のクラスのインスタンスであり、LastPositionは前の位置です

コライダーはコライダー(あなたの健康に影響を与えるポイント)



Players構造には、構造のインスタンスを処理する/サーバーに送信するインスタンスを準備するメソッドが含まれている必要があります。これらのタスクでは、次のメソッドを使用します。



 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)
      
      





1つ目の方法は、プレーヤーから一定量のヘルスを取得する方法です。

2番目の方法は、プレーヤーに新しい位置を割り当てるために必要です。



タンクを作成するときにエラーが発生しないように、コンストラクタで3番目を使用します。

4番目はショットを発射します(つまり、戦車から弾丸を発射します)。



5番目はプレーヤーの前の位置にテキストを印刷する必要があります(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; } }
      
      





ここで条件を組み合わせて、別のプレイヤー(コンソールで突然ラグが発生する)が競技場を離れることができないようにします。 ちなみに、使用するフィールド(高さと幅)は、フィールドの境界(高さと幅)を示します。



3番目:



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





ここでは、座標を競技場のサイズより小さくすることはできません。



ちなみに、コンソールでは、座標系は左上隅(ポイント0; 0)から始まり、xで80、yで80に制限(設定可能)があります。 xが80に達するとクラッシュし(つまり、アプリケーションが壊れます)、80がyになると、フィールドのサイズが単純に増加します(コンソールをクリックしてプロパティを選択すると、これらの制限を構成できます)。



5日:



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





ここでは、テキストを前の位置に印刷します(ペイントします)。



シェルの構造をまだ発表していないため、4番目の方法はありません。

彼女の話をしましょう。



シェル構造(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、損傷はこの発射物の損傷、x_wayはXの動き、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; }
      
      





最初の方法-このパスを持つコレクションを使用して、コンソール内のパスを忘れます(つまり、ペイントします)。



次はコンストラクター(つまり、構造体のインスタンスを構成するメインメソッド)です。



3番目の方法-すべての情報をテキスト形式で表示し、送信時にのみ使用されます

サーバーに。



さらにメソッドは、将来の使用のためにいくつかのフィールドを印刷/返します。



構造「Wall(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'メソッドは、コンストラクターからシェルを作成します。



構造的相互作用



メインスレッドには2つの並列スレッド(タスク)があり、それらに基づいて構築します。



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





どんな流れ? 最初はサーバーからデータを受信して​​処理し、2番目はサーバーにデータを送信します。



そのため、サーバーをリッスン(つまり、サーバーからデータを受信)し、受信したデータを処理し、送信されたデータに対して操作を実行する必要があります。



プロジェクトのサーバーから受信したデータはすべて、引数(イベント名など)が「:」記号で区切られたイベントオブジェクトです。つまり、出力では次のスキームがあります。EventName:Arg1:Arg2:Arg3:... ArgN



したがって、2つのタイプのイベント(それらは不要になったため)と、プロジェクトの構造要素との相互作用、つまりタンクの動きと作成+発射物の動きもあります。



しかし、私たちはまだ何を処理するのかではなく、このデータを受信する方法を知らないので、最も美しいサイトに移動し(記事の下部にあるリンク)、ネットワークとソケット(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()」メソッドでこの配列を「データ」変数に収集します、その後、新しいスレッド(非同期、つまり、このメソッドでのタスクの実行に関係なく実行される)と「Main」メソッドを作成し、記述して開始します(「.Start()」メソッドを使用)。



コード付きの画像形式の小さな説明(このアイデアをテストするためにこのコードを使用しました)、このコードはプロジェクトとは関係なく、そのコードをテストし(同様に)、1つの非常に重要なタスクを解決するために作成されました:コードは基本的にメソッドです。」 ネタバレ:はい!



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





そして、彼の作品は次のとおりです。



画像



実行中の4つのスレッド(そのうちの1つはほぼ仕事を終えています):



画像



ストリームによって実行されるメソッドにさらに、またはむしろ移動します。



  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」の場合、「Walls」と「Tank」の要素のみが相互作用します。



ただし、イベントタイプが「createshot」の場合、すべてが文字通り相互作用します。



ショットが壁に触れた場合-それが彼女の体力を奪い、ショットがプレイヤーに触れた場合-そしてショットが飛び去った場合、彼は彼の体力を取りました-そしてそれは消えてクリアされました。



別のイベントがある場合は、このメソッドを終了します。すべてがシンプルに思えます。

しかし、すべてがそれほど単純ではないので、呼び出されたメソッドをより深く、より正確に掘り下げると、ジュース自体が始まります。



これらのメソッドの名前から、1つ目は戦車の動き、つまりコライダーの描画と移動であり、2つ目はショットの作成と発射であることは明らかです。



タンクを引く方法:



 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」のカウントが始まる時間(ゼロミリ秒(1000ミリ秒(ミリ秒))- 1s(秒))およびメソッド呼び出し間隔(ミリ秒)は '10'です(ちなみに、 'To_key()'メソッドは完全にリロードするように構成されています(これは、プログラムクラスのフィールドに関連付けられている場合の条件で表されます))。



このメソッドは次のようになります。



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





「クールダウン」はリロード(ショット)です。



それでも、このプロジェクトのほとんどの要素はフィールドです。



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





最後に終わり



これでtankプロジェクトのコードの説明は終了です。サーバー(壁、戦車(プレイヤー))からのデータのロード、および私たちの戦車への座標の割り当てを除いて、ほぼすべてを実装できました。 これについては、次の記事で対処します。次の記事では、すでにサーバーについて触れます。



記事に記載されているリンク:



私のリポジトリ

このコードを書いて新しいことを教えてくれたクールなプログラマー: Habra-Mikhailnorver



この記事を読んでくれてありがとう、私があなたの意見に何らかの形で間違っているなら-コメントに書いてください、そして私たちは一緒にそれを変えます。



また、プロジェクトとコードの両方の改善について議論しているため、コメントにも注意してください。 本の翻訳を手伝いたい場合は、メッセージまたはメールで私にメールしてください:koito_tyan@mail.ru。



ゲームを始めましょう!



All Articles