ヘビを所有しているか、最初のドラフトを書いています。 パート0

まえがき



こんにちはHabr! 私の名前はEugene "Nage"です。1年ほど前、暇なときにプログラミングを始めました。 多くの異なるプログラミングチュートリアルを見た後、「次に何をすべきか」という質問を自問自答します。 同じことに関するさまざまなビデオを長い間見た後、私はそれが最初のプロジェクトに進んで取り組む価値があると判断しました。 そして、今、私たちは最初の知識でコンソールでゲーム「Snake」を書く方法を分析します。



第1章では、どこから始めますか?



まず、メモ帳(またはお気に入りのエディター)とC#コンパイラーだけを追加する必要はありません。Windowsにはデフォルトで存在し、C:\ Windows \ Microsoft.NET \ Framework \ v4.0.30319 \ csc.exeにあります。 Visual Studioに付属する最新のコンパイラを使用できます。VisualStudioは、Microsoft Visual Studio \ 2017 \ Community \ MSBuild \ 15.0 \ Bin \ Roslyn \ csc.exeにあります。



コードをすばやくコンパイルするためのファイルを作成し、次の内容の拡張子.batでファイルを保存します。



@echo off :Start set /p name= Enter program name: echo. :\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe "%name%.cs" echo. goto Start
      
      





「@echo off」は、コンソールでのコマンドの表示を無効にします。 gotoコマンドを使用すると、無限ループになります。 name変数を設定し、/ p修飾子を使用して、ユーザーがコンソールに入力した値が変数に書き込まれます。 「エコー。」コンソールに空の行を残すだけです。 次に、コンパイラを呼び出して、コンパイルしたコードのファイルを渡します。



この方法では、1つのファイルしかコンパイルできないため、すべてのクラスを1つのドキュメントに記述します(コンソールを介して複数のファイルを1つの.exeにコンパイルする方法はまだわかりません。これは記事のトピックではありません。コメントで教えてください)。



コード全体をすぐに見たい人向け。



非表示のテキスト
 using System; using System.Threading; using System.Collections.Generic; using System.Linq; namespace SnakeGame { class Game { static readonly int x = 80; static readonly int y = 26; static Walls walls; static Snake snake; static FoodFactory foodFactory; static Timer time; static void Main() { Console.SetWindowSize(x + 1, y + 1); Console.SetBufferSize(x + 1, y + 1); Console.CursorVisible = false; walls = new Walls(x, y, '#'); snake = new Snake(x / 2, y / 2, 3); foodFactory = new FoodFactory(x, y, '@'); foodFactory.CreateFood(); time = new Timer(Loop, null, 0, 200); while (true) { if (Console.KeyAvailable) { ConsoleKeyInfo key = Console.ReadKey(); snake.Rotation(key.Key); } } }// Main() static void Loop(object obj) { if (walls.IsHit(snake.GetHead()) || snake.IsHit(snake.GetHead())) { time.Change(0, Timeout.Infinite); } else if (snake.Eat(foodFactory.food)) { foodFactory.CreateFood(); } else { snake.Move(); } }// Loop() }// class Game struct Point { public int x { get; set; } public int y { get; set; } public char ch { get; set; } public static implicit operator Point((int, int, char) value) => new Point {x = value.Item1, y = value.Item2, ch = value.Item3}; public static bool operator ==(Point a, Point b) => (ax == bx && ay == by) ? true : false; public static bool operator !=(Point a, Point b) => (ax != bx || ay != by) ? true : false; public void Draw() { DrawPoint(ch); } public void Clear() { DrawPoint(' '); } private void DrawPoint(char _ch) { Console.SetCursorPosition(x, y); Console.Write(_ch); } } class Walls { private char ch; private List<Point> wall = new List<Point>(); public Walls(int x, int y, char ch) { this.ch = ch; DrawHorizontal(x, 0); DrawHorizontal(x, y); DrawVertical(0, y); DrawVertical(x, y); } private void DrawHorizontal(int x, int y) { for (int i = 0; i < x; i++) { Point p = (i, y, ch); p.Draw(); wall.Add(p); } } private void DrawVertical(int x, int y) { for (int i = 0; i < y; i++) { Point p = (x, i, ch); p.Draw(); wall.Add(p); } } public bool IsHit(Point p) { foreach (var w in wall) { if (p == w) { return true; } } return false; } }// class Walls enum Direction { LEFT, RIGHT, UP, DOWN } class Snake { private List<Point> snake; private Direction direction; private int step = 1; private Point tail; private Point head; bool rotate = true; public Snake(int x, int y, int length) { direction = Direction.RIGHT; snake = new List<Point>(); for (int i = x - length; i < x; i++) { Point p = (i, y, '*'); snake.Add(p); p.Draw(); } } public Point GetHead() => snake.Last(); public void Move() { head = GetNextPoint(); snake.Add(head); tail = snake.First(); snake.Remove(tail); tail.Clear(); head.Draw(); rotate = true; } public bool Eat(Point p) { head = GetNextPoint(); if (head == p) { snake.Add(head); head.Draw(); return true; } return false; } public Point GetNextPoint () { Point p = GetHead (); switch (direction) { case Direction.LEFT: px -= step; break; case Direction.RIGHT: px += step; break; case Direction.UP: py -= step; break; case Direction.DOWN: py += step; break; } return p; } public void Rotation (ConsoleKey key) { if (rotate) { switch (direction) { case Direction.LEFT: case Direction.RIGHT: if (key == ConsoleKey.DownArrow) direction = Direction.DOWN; else if (key == ConsoleKey.UpArrow) direction = Direction.UP; break; case Direction.UP: case Direction.DOWN: if (key == ConsoleKey.LeftArrow) direction = Direction.LEFT; else if (key == ConsoleKey.RightArrow) direction = Direction.RIGHT; break; } rotate = false; } } public bool IsHit(Point p) { for (int i = snake.Count - 2; i > 0; i--) { if (snake[i] == p) { return true; } } return false; } }//class Snake class FoodFactory { int x; int y; char ch; public Point food { get; private set; } Random random = new Random(); public FoodFactory(int x, int y, char ch) { this.x = x; this.y = y; this.ch = ch; } public void CreateFood() { food = (random.Next(2, x - 2), random.Next(2, y - 2), ch); food.Draw(); } } }
      
      







第2章最初のステップ



プログラムへのエントリーポイントから始めて、ゲームのフィールドを準備します。 変数XとY、コンソールウィンドウのサイズとバッファを設定し、カーソル表示を非表示にします。



 using System; using System.Collections.Generic; using System.Linq; class Game{ static readonly int x = 80; static readonly int y = 26; static void Main(){ Console.SetWindowSize(x + 1, y + 1); Console.SetBufferSize(x + 1, y + 1); Console.CursorVisible = false; }// Main() }// class Game
      
      





画面に「グラフィック」を表示するには、独自のデータ型-期間を作成します。 画面に表示される座標と文字が含まれます。 また、ポイントとその「消去」を表示するメソッドも作成します。



 struct Point{ public int x { get; set; } public int y { get; set; } public char ch { get; set; } public static implicit operator Point((int, int, char) value) => new Point {x = value.Item1, y = value.Item2, ch = value.Item3}; public void Draw(){ DrawPoint(ch); } public void Clear(){ DrawPoint(' '); } private void DrawPoint(char _ch){ Console.SetCursorPosition(x, y); Console.Write(_ch); } }
      
      





これは面白いです!

=>演算子はラムダ演算子と呼ばれ、匿名ラムダ式の定義として使用され、 1つの式の本体として構文シュガーがreturnステートメントを置き換えます。 演算子を再定義する上記の方法(その目的についてはやや低い)は、次のように書き直すことができます。



 public static bool operator ==(ポイントa、ポイントb){
     if(ax == bx && ay == by){
         trueを返します。
     }
    その他{
         falseを返します。
     }
 } 




競技場の境界である壁のクラスを作成します。 垂直線と水平線を作成するための2つのメソッドを記述し、コンストラクターで、指定された文字で4辺すべての描画を呼び出します。 壁のすべてのポイントのリストは、後で役立ちます。



 class Walls{ private char ch; private List<Point> wall = new List<Point>(); public Walls(int x, int y, char ch){ this.ch = ch; DrawHorizontal(x, 0); DrawHorizontal(x, y); DrawVertical(0, y); DrawVertical(x, y); } private void DrawHorizontal(int x, int y){ for (int i = 0; i < x; i++){ Point p = (i, y, ch); p.Draw(); wall.Add(p); } } private void DrawVertical(int x, int y) { for (int i = 0; i < y; i++) { Point p = (x, i, ch); p.Draw(); wall.Add(p); } } }// class Walls
      
      





これは面白いです!



お気づきかもしれませんが、Point p =(x、y、ch)という形式を使用してPointデータ型を初期化します。 組み込み型と同様に、これは、変数の設定方法を記述する暗黙の演算子をオーバーライドすることで可能になります。



重要!



構造(int、int、char)はタプルと呼ばれ、.net 4.7+でのみ動作します。したがって、Visual Studioがインストールされていない場合は、v4.0.30319コンパイラのみがあり、new演算子を使用して標準の初期化を使用する必要があります。



Gameクラスに戻って、wallsフィールドを宣言し、Mainメソッドで初期化します。



 class Game{ static Walls walls; static void Main(){ walls = new Walls(x, y, '#'); ...
      
      





それだけです! コードをコンパイルすると、フィールドが構築されており、最も簡単な部分が背後にあることがわかります。



第3章今日の朝食はどうですか?



食品の生成をフィールドに追加します。これのために、FoodFactoryクラスを作成します。このクラスは、国境内での食品の作成に関与します。



 class FoodFactory { int x; int y; char ch; public Point food { get; private set; } Random random = new Random(); public FoodFactory(int x, int y, char ch) { this.x = x; this.y = y; this.ch = ch; } public void CreateFood() { food = (random.Next(2, x - 2), random.Next(2, y - 2), ch); food.Draw(); } }
      
      





工場での初期化を追加し、フィールドに食べ物を作成

 class Game{ static FoodFactory foodFactory; static void Main(){ foodFactory = new FoodFactory(x, y, '@'); foodFactory.CreateFood(); ...
      
      





食べました!



第4章主人公の時代



ヘビ自体の作成に進みます。まず、ヘビの移動方向の列挙を決定します。



 enum Direction{ LEFT, RIGHT, UP, DOWN }
      
      





これで、ヘビクラスを作成し、クロール、回転の方法を説明できます。 スネークポイントのリスト、列挙、ターンごとの移動量のステップ、テールポイントとヘッドポイントへのリンク、およびゲームの開始時に特定の座標と特定の長さでスネークを描画するコンストラクターを定義します。



 class Snake{ private List<Point> snake; private Direction direction; private int step = 1; private Point tail; private Point head; bool rotate = true; public Snake(int x, int y, int length){ direction = Direction.RIGHT; snake = new List<Point>(); for (int i = x - length; i < x; i++) { Point p = (i, y, '*'); snake.Add(p); p.Draw(); } } //         . public Point GetHead() => snake.Last(); public void Move(){ head = GetNextPoint(); snake.Add(head); tail = snake.First(); snake.Remove(tail); tail.Clear(); head.Draw(); rotate = true; } public Point GetNextPoint() { Point p = GetHead(); switch (direction) { case Direction.LEFT: px -= step; break; case Direction.RIGHT: px += step; break; case Direction.UP: py -= step; break; case Direction.DOWN: py += step; break; } return p; } public void Rotation(ConsoleKey key) { if (rotate) { switch (direction) { case Direction.LEFT: case Direction.RIGHT: if (key == ConsoleKey.DownArrow) direction = Direction.DOWN; else if (key == ConsoleKey.UpArrow) direction = Direction.UP; break; case Direction.UP: case Direction.DOWN: if (key == ConsoleKey.LeftArrow) direction = Direction.LEFT; else if (key == ConsoleKey.RightArrow) direction = Direction.RIGHT; break; } rotate = false; } } }//class Snake
      
      





回転方法では、すぐに180度回転する可能性を回避するために、各方向で2方向にしか回転できないことを示します。 そして、2回のタップで180度回転する問題-「スイッチ」を入れて、最初に押した後にオンにする機能をオフにし、次の移動後にオンにする機能。



画面に表示するために残ります。



 class Game{ static Snake snake; static void Main(){ snake = new Snake(x / 2, y / 2, 3); ...
      
      





できた! 今、私たちは必要なものすべて、壁に囲まれたフィールド、ランダムに現れる食べ物、そしてヘビを持っています。 すべてを相互にやり取りする時が来ました。



第5章L-logic



スネークを動かし、キーボードで押されたキーを読み取るための無限ループを作成し、キーをスネーク回転メソッドに渡しましょう



 class Game { static void Main () { while (true) { if (Console.KeyAvailable) { ConsoleKeyInfo key = Console.ReadKey (); snake.Rotation(key.Key); } ...
      
      





ヘビを移動するには、特定の間隔でLoopメソッドを実行する.netクラスを使用します。



 using System.Threading; class Game { static Timer time; static void Main () { time = new Timer (Loop, null, 0, 200); ...
      
      





さて、ヘビを動かす方法を書く前に、頭と食物、ヘビの壁と尾との相互作用を実装する必要があります。 これを行うには、座標の一致について2つのポイントを比較できるメソッドを記述します。 等号と非等号の演算子を再定義します。それらはペアで再定義する必要があります。



 struct Point { public static bool operator == (Point a, Point b) => (ax == bx && ay == by) ? true : false; public static bool operator != (Point a, Point b) => (ax != bx || ay != by) ? true : false; ...
      
      





これで、関心のあるポイントが壁の配列のいずれかと一致するかどうかをチェックするメソッドを作成できます。

 class Walls { public bool IsHit (Point p) { foreach (var w in wall) { if (p == w) { return true; } } return false; } ...
      
      





同様の方法で、ポイントがテールと一致するかどうかを確認します。



 class Snake { public bool IsHit (Point p) { for (int i = snake.Count - 2; i > 0; i--) { if (snake[i] == p) { return true; } } return false; } ...
      
      





そして、私たちのヘビが食べ物を食べたかどうかをチェックすることで、すぐにそれを長くします。



 class Snake { public bool Eat (Point p) { head = GetNextPoint (); if (head == p) { snake.Add (head); head.Draw (); return true; } return false; } ...
      
      





これで、必要なすべてのチェックを含む移動方法を作成できます。



 class Snake { static void Loop (object obj) { if (walls.IsHit (snake.GetHead ()) || snake.IsHit (snake.GetHead ())) { time.Change (0, Timeout.Infinite); } else if (snake.Eat (foodFactory.food)) { foodFactory.CreateFood (); } else { snake.Move (); } } ...
      
      





以上です! コンソールのヘビが完成し、プレイできます。





おわりに



OOPを少し使用するだけで最初のシンプルなゲームを実装する方法を検討し、演算子をオーバーロードする方法を学習し、タプルとラムダ演算子を調べました。



これはパイロット記事であり、気に入った場合は、Unityでのヘビの実装について説明します。

皆さんに幸運を!



All Articles