Объектно-ориентированная игра Змейка

Прямо сейчас у меня есть игра-змейка, которая работает, но мне нужен способ, чтобы мой код выглядел лучше и работал лучше.

Метод, который я назвал GameSense очень длинный, и мне нужен способ сделать его более объектно-ориентированным. Я хочу использовать перечисления.

Вот что мне прислал мой учитель:

В игре очень красивый и красиво рабочий хвостик! Ваш тест также следует правильной идее, но использует UpdatePosition метод, который вы не реализовали. Я также думаю о пикселе вашего класса — вы не используете его ни для чего в остальной части игры. Вы должны использовать его вместо, например, berryx, berryy. Я пытаюсь получить доступ к большему использованию объектной ориентации и разделения на методы, а не к одному длинному методу вроде GameSense прямо сейчас. Также хочу увидеть, что вы используете enum / bool вместо строк, например, для переменных movement и buttonpressed. Тем не менее, я рад, что у вас есть интерфейс на углу

Как видите… я чувствую себя отстойно прямо сейчас.
Что мне здесь нужно сделать, чтобы программа стала более объектно-ориентированной?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace Snake
{
    /// <summary>
    /// Start class, everything goes from here.
    /// Made by Karwan!
    /// </summary>
   public class Starter : InterF
    { 
        public static object Key { get; private set; }

        /// <summary>
        /// Main method.
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            //Making the text green and also given a player 
            //options to chose from. 
            Console.ForegroundColor = ConsoleColor.Green;
            bool loopC = true;
            while (loopC)
            {
                string mystring = null; Console.WriteLine("Welcome to Snake.nPress S+Enter to start or press Q+Enter to exit.");
                mystring = Console.ReadLine();
                switch (mystring)
                {
                    case "Q":
                        Environment.Exit(0);
                        break;
                    case "S":
                        Console.WriteLine("Starting game!");
                        System.Threading.Thread.Sleep(4000);
                        loopC = false;
                        break;
                    default:
                        Console.WriteLine("Invalid Entry.. Try again.");
                        break;
                }

            }

            //Call for GameSense if
            //they want to play.
            GameSense(); 

        }


      
        /// <summary>
        /// Game object!
        /// </summary>
        public static void GameSense()

        {
            //Game console height/width.
            Console.WindowHeight = 30;
            Console.WindowWidth = 70;
            int screenwidth = Console.WindowWidth;
            int screenheight = Console.WindowHeight;
            Random randomnummer = new Random();
            //Lenght of tail == current score.
            int score = 2;
            int gameover = 0;
            //Gather positions from pixel class.
            pixel positions = new pixel();
            positions.xpos = screenwidth / 2;
            positions.ypos = screenheight / 2;
            positions.Black = ConsoleColor.Red;
            string movement = "RIGHT";
            //Position manegment.
            List<int> xpos = new List<int>();
            List<int> ypos = new List<int>();
            int berryx = randomnummer.Next(0, screenwidth);
            int berryy = randomnummer.Next(0, screenheight);
            //Time manegment.
            DateTime time1 = DateTime.Now;
            DateTime time2 = DateTime.Now;
            string buttonpressed = "no";

            
            //Draw world from GameWorld.cs.
            GameWorld.DrawBorder(screenwidth, screenheight);

            while (true)
            {
                GameWorld.ClearConsole(screenwidth, screenheight);
                if (positions.xpos == screenwidth - 1 || positions.xpos == 0 || positions.ypos == screenheight - 1 || positions.ypos == 0)
                {
                    gameover = 1;
                }

                Console.ForegroundColor = ConsoleColor.Green;
                if (berryx == positions.xpos && berryy == positions.ypos)
                {
                    score++;
                    berryx = randomnummer.Next(1, screenwidth - 2);
                    berryy = randomnummer.Next(1, screenheight - 2);
                }
                for (int i = 0; i < xpos.Count(); i++)
                {
                    Console.SetCursorPosition(xpos[i], ypos[i]);
                    Console.Write("*");
                    if (xpos[i] == positions.xpos && ypos[i] == positions.ypos)
                    {
                        gameover = 1;
                    }
                }
                if (gameover == 1)
                {
                    break;
                }
                Console.SetCursorPosition(positions.xpos, positions.ypos);
                Console.ForegroundColor = positions.Black;
                Console.Write("*");
                //Food color & position.
                Console.SetCursorPosition(berryx, berryy);
                Console.ForegroundColor = ConsoleColor.Cyan;
                Console.Write("*");
                Console.CursorVisible = false;
                time1 = DateTime.Now;
                buttonpressed = "no";
                while (true)
                {
                    time2 = DateTime.Now;
                    if (time2.Subtract(time1).TotalMilliseconds > 500) { break; }
                    if (Console.KeyAvailable)
                    {
                        ConsoleKeyInfo info = Console.ReadKey(true);
                        //Connecting the buttons to the x/y movments. 
                        if (info.Key.Equals(ConsoleKey.UpArrow) && movement != "DOWN" && buttonpressed == "no")
                        {
                            movement = "UP";
                            buttonpressed = "yes";
                        }
                        if (info.Key.Equals(ConsoleKey.DownArrow) && movement != "UP" && buttonpressed == "no")
                        {
                            movement = "DOWN";
                            buttonpressed = "yes";
                        }
                        if (info.Key.Equals(ConsoleKey.LeftArrow) && movement != "RIGHT" && buttonpressed == "no")
                        {
                            movement = "LEFT";
                            buttonpressed = "yes";
                        }
                        if (info.Key.Equals(ConsoleKey.RightArrow) && movement != "LEFT" && buttonpressed == "no")
                        {
                            movement = "RIGHT";
                            buttonpressed = "yes";
                        }
                    }
                }
                //Giving the connections value
                //to change x/y to make the movment happen.
                xpos.Add(positions.xpos);
                ypos.Add(positions.ypos);
                switch (movement)
                {
                    case "UP":
                        positions.ypos--;
                        break;
                    case "DOWN":
                        positions.ypos++;
                        break;
                    case "LEFT":
                        positions.xpos--;
                        break;
                    case "RIGHT":
                        positions.xpos++;
                        break;
                }
                if (xpos.Count() > score)
                {
                    xpos.RemoveAt(0);
                    ypos.RemoveAt(0);
                }
            }
            Console.SetCursorPosition(screenwidth / 5, screenheight / 2);
            Console.WriteLine("Game over, Score: " + score);
            Console.SetCursorPosition(screenwidth / 5, screenheight / 2 + 1);
            System.Threading.Thread.Sleep(1000);
            restart();
            
        }

        /// <summary>
        /// Restarter.
        /// </summary>
        public static void restart()
        {
        
            string Over = null; Console.WriteLine("nWould you like to start over? Y/N");
            bool O = true;

            while (O)
            {
                Over = Console.ReadLine();
                switch (Over)
                {
                    case "Y":
                        Console.WriteLine("nRestarting!");
                        System.Threading.Thread.Sleep(2000);
                        break;
                    case "N":
                        Console.WriteLine("nThank you for playing!");
                        Environment.Exit(0);
                        break;
                    default:
                        Console.WriteLine("Invalid Entry.. Try again.");
                        break;
                }
            }

        }
        /// <summary>
        /// Set/get pixel position.
        /// </summary>
       public class pixel
        {
            public int xpos { get; set; }
            public int ypos { get; set; }
            public ConsoleColor Black { get; set; }
        }
    }
}

Мой тестовый класс

using Microsoft.VisualStudio.TestTools.UnitTesting;
using NUnit.Framework;
using Snake;
using System;
using System.Collections.Generic;
using System.Text;
using static Snake.Starter;

namespace Snake.Tests
{
    [TestClass()]
    public class StarterTests
    {
        /// <summary>
        /// Testing the movment.
        /// </summary>
        [TestMethod()]
        public void test()
        {
           
           var pos = new pixel();
            pos.xpos = 10;
            pos.ypos = 10;

            // this is the operation:
            UpdatePosition(pos, "UP");

            // check the results
            NUnit.Framework.Assert.That(pos.xpos, Is.EqualTo(10));
            NUnit.Framework.Assert.That(pos.ypos, Is.EqualTo(9));
        }

        /// <summary>
        /// Updating pixel class.
        /// </summary>
        /// <param name="pos"></param>
        /// <param name="v"></param>
        private void UpdatePosition(pixel pos, string v)
        {
            throw new NotImplementedException();
        }
    }
}

Это просто игровой мир:

using System;
using System.Linq;

public class GameWorld 
{


    /// <summary>
    /// Clear console for snake.
    /// </summary>
    /// <param name="screenwidth"></param>
    /// <param name="screenheight"></param>
    public static void ClearConsole(int screenwidth, int screenheight)
    {
        var blackLine = string.Join("", new byte[screenwidth - 2].Select(b => " ").ToArray());
        Console.ForegroundColor = ConsoleColor.Black;
        for (int i = 1; i < screenheight - 1; i++)
        {
            Console.SetCursorPosition(1, i);
            Console.Write(blackLine);
        }
    }

    /// <summary>
    /// Draw boared.
    /// </summary>
    /// <param name="screenwidth"></param>
    /// <param name="screenheight"></param>
    public static void DrawBorder(int screenwidth, int screenheight)
    {
        var horizontalBar = string.Join("", new byte[screenwidth].Select(b => "■").ToArray());

        Console.SetCursorPosition(0, 0);
        Console.Write(horizontalBar);
        Console.SetCursorPosition(0, screenheight - 1);
        Console.Write(horizontalBar);

        for (int i = 0; i < screenheight; i++)
        {
            Console.SetCursorPosition(0, i);
            Console.Write("■");
            Console.SetCursorPosition(screenwidth - 1, i);
            Console.Write("■");
        }
    }

}

1 ответ
1

ООП — Группирование данных, которые принадлежат друг другу

Я также думаю о пикселе вашего класса — вы не используете его ни для чего в остальной части игры. Вы должны использовать его, например, вместо ягодного, ягодного.

Вы сохраняете позиции X и Y в несвязанных списках:

List<int> xpos = new List<int>();
List<int> ypos = new List<int>();
int berryx = randomnummer.Next(0, screenwidth);
int berryy = randomnummer.Next(0, screenheight);

Но раньше вы группировали позицию (то есть координату XY) в одном объекте:

pixel positions = new pixel();
positions.xpos = screenwidth / 2;
positions.ypos = screenheight / 2;

Ваш учитель, по сути, говорит вам делать то же самое для всех координат X и Y.

Я собираюсь представить здесь новый класс под названием Position. Это похоже на твою Pixel класс, но без указания цвета. Цель Position класс должен представлять одну координату (X, Y):

public class Position
{
    public int X { get; set; }
    public int Y { get; set; }
}

Теперь, когда у вас есть этот класс, вы можете начать использовать его везде, где вы в настоящее время используете отдельные значения X и Y:

// Instead of:

List<int> xpos = new List<int>();
List<int> ypos = new List<int>();  

// Use:

List<Position> positions = new List<Position>();  

// Instead of:

int berryx = randomnummer.Next(0, screenwidth);
int berryy = randomnummer.Next(0, screenheight);

// Use:

Position berryPosition = new Position()
{
    X = randomnummer.Next(0, screenwidth),
    Y = randomnummer.Next(0, screenheight)
}; 

Отметим также, что Pixel класс можно настроить для повторного использования этого Position:

public class Pixel
{
    public Position Position { get; set; }
    public ConsoleColor Color { get; set; }
}

Я оставлю дальнейшие настройки (то есть, как вы получите доступ к этим данным позже) в качестве упражнения для вас. Это ничем не отличается от того, как вы получали доступ к своему Pixel данные уже.


ООП — методы класса

Помимо хранения значений данных, классы могут иметь методы. Методы класса отлично подходят для размещения любой логики, принадлежащей объекту.

Например, ягоды должны иметь возможность определять, как они определяют свою позицию. Прямо сейчас вы просто используете случайный выбор, но со временем это может измениться. Например, ягоды могут не появляться на границах или рядом с их предыдущим положением.
Какой бы ни была причина, главный вывод здесь состоит в том, что было бы практично, если бы ягода содержала «логику генерирования положения ягоды».

Для простоты предположим, что одна и та же ягода не меняет положение, когда ее едят, а вы всегда делаете new Berry (подсказка!) всякий раз, когда съедается предыдущий. Следовательно, положение новой ягоды можно определить в конструкторе класса.

я собираюсь нет сделать то, что мы могли сделать. Попробуйте найти это. Поправлю позже.

public class Berry
{
    public int X { get; set; }
    public int Y { get; set; }

    public Berry(int screenWidth, int screenHeight)
    {
        var rand = new Random();
        this.X = GenerateX(rand, screenwidth);
        this.Y = GenerateY(rand, screenHeight);
    }

    private int GenerateX(Random rand, int screenWidth)
    {
         return rand.Next(0, screenWidth); 
    }

    private int GenerateY(Random rand, int screenHeight)
    {
         return rand.Next(0, screenHeight); 
    }
}

Вы заметили, что мы можем улучшить? Вернитесь к предыдущему разделу.

Мы создали Position класс специально для представления координат XY. Мы должны быть повторное использование вот эта логика!

public class Berry
{
    public Position Position { get; private set; }

    public Berry(int screenWidth, int screenHeight)
    {
        var rand = new Random();
        this.Position = GeneratePosition(rand, screenWidth, screenHeight);
    }

    private int GeneratePosition(Random rand, int screenWidth, int screenHeight)
    {
         return new Position()
         {
             X = rand.Next(0, screenWidth),
             Y = rand.Next(0, screenHeight)
         }; 
    }
}

В вашем коде есть еще одна логика, специфичная для ягод: рисование ее на экране. У ягод свой цвет. Это принадлежит Berry учебный класс.

Обратите внимание, что я опускаю Berry код из предыдущего фрагмента, чтобы он оставался сфокусированным, но они, конечно, должны быть объединены вместе в реальном коде.

public class Berry
{
    // ...

    public ConsoleColor Color = ConsoleColor.Cyan;

    public void Draw()
    {
        Console.SetCursorPosition(this.Position.X, this.Position.Y);
        Console.ForegroundColor = this.Color;
        Console.Write("*");
        Console.CursorVisible = false;
    }
}

Обратите внимание, как мы полагались на то, что Berry объект уже знает свое положение и цвет, поэтому мы смогли написать наш код, чтобы просто получить эту информацию.

Это открывает дверь для небольшого аккуратного улучшения. Так же, как мы меняем положение ягоды случайным образом, почему бы не изменить ее цвет? Если вы посмотрите на Draw() логики, вы увидите, что он просто использует цвет в this.Color. Итак, если мы изменим значение this.Color, напечатанный символ изменит цвет (нам не нужно менять цвет Draw() сам метод).

Я собираюсь ограничить возможный диапазон цветов списком, который мы предварительно выбрали. Это лучше, чем дикий случайный. Также обратите внимание на этот ответ StackOverflow со многими решениями о том, как случайным образом выбрать цвет.

Это объединенная логика всего, что мы обсуждали:

public class Berry
{
    public Position Position { get; private set; }
    public ConsoleColor Color { get; private set; }

    private readonly ConsoleColor[] _allowedColors = new ConsoleColor[]
    {
        ConsoleColor.Cyan, ConsoleColor.Yellow, ConsoleColor.Red, ConsoleColor.Green
    };

    public Berry(int screenWidth, int screenHeight)
    {
        var rand = new Random();
        this.Position = GeneratePosition(rand, screenWidth, screenHeight);
        this.Color = GenerateColor(rand);
    }

    private int GeneratePosition(Random rand, int screenWidth, int screenHeight)
    {
         return new Position()
         {
             X = rand.Next(0, screenWidth),
             Y = rand.Next(0, screenHeight)
         }; 
    }

    private ConsoleColor GenerateColor(Random rand)
    {
        return _allowedColors.ElementAt(random.Next(_allowedColors .Length));
    }

    public void Draw()
    {
        Console.SetCursorPosition(this.Position.X, this.Position.Y);
        Console.ForegroundColor = this.Color;
        Console.Write("*");
        Console.CursorVisible = false;
    }
}

Это объектно-ориентированная ягода. Теперь ваша игровая логика должна минимально взаимодействуют с ягодой:

var berry = new Berry(Console.WindowWidth, Console.WindowHeight);

berry.Draw();

Все остальное содержится в Berry учебный класс. Подсказка: вот почему они называют такой стиль кодирования «инкапсуляцией». Ягодная логика содержится в Berry class и скрыт от глаз, чтобы остальная часть кода не отвлекалась на него.


Волшебные струны

Также хочу увидеть, что вы используете enum / bool вместо строк, например, для переменных movement и buttonpressed.

Строки хороши и читабельны, но они также не очень эффективны с точки зрения памяти (по сравнению с другими типами данных) и допускают много двусмысленности, например, как "right" и "Right" это два разных значения.

Вам следует использовать типы данных, соответствующие тому, что вы пытаетесь сделать.

buttonpressed явно логическое значение. Самый простой способ придумать логическое значение — это представить себе выключатель света. У него есть только две возможные позиции. Будь то «да / нет», «вкл / выкл», «истина / ложь», «жив / мертв» … семантически и не имеет значения. В C # вы просто используете true и false

bool buttonPressed = false;

if(buttonPressed)
    Console.WriteLine("This WON'T be printed because it's false");

buttonPressed = true;

if(buttonPressed)
    Console.WriteLine("This WILL be printed because it's true");

Я подозреваю, что ваш учитель дал вам учебный материал о таких типах данных, как логические значения, поэтому я предлагаю вам отредактировать этот материал.

movement отличается. Здесь вы не имеете дело с чем-то, что легко выразить с помощью известного типа данных. Вы имеете дело с закрытым списком из нескольких вариантов, поэтому enum здесь уместно.

public enum Direction { Up, Down, Left, Right }

Цель вашего кода останется прежней, но вы будете использовать более качественные типы данных, которые лучше соответствуют вашим потребностям:

//Connecting the buttons to the x/y movments. 
if (info.Key.Equals(ConsoleKey.UpArrow) && movement != Direction.Down && !buttonpressed)
{
    movement = Direction.Up;
    buttonpressed = true;
}
if (info.Key.Equals(ConsoleKey.DownArrow) && movement != Direction.Up && !buttonpressed)
{
    movement = Direction.Down;
    buttonpressed = true;
}
if (info.Key.Equals(ConsoleKey.LeftArrow) && movement != Direction.Right && !buttonpressed)
{
    movement = Direction.Left;
    buttonpressed = true;
}
if (info.Key.Equals(ConsoleKey.RightArrow) && movement != Direction.Left && !buttonpressed)
{
    movement = Direction.Right;
    buttonpressed = true;
}

Я ушел buttonpressed пока, чтобы показать вам синтаксис. Однако на самом деле он вам здесь не нужен. Если бы вы использовали if else вместо if, вы бы автоматически были уверены, что только один Возможный исход был бы достигнут, а не множественный.

if (info.Key.Equals(ConsoleKey.UpArrow) && movement != Direction.Down)
{
    movement = Direction.Up;
}
else if (info.Key.Equals(ConsoleKey.DownArrow) && movement != Direction.Up)
{
    movement = Direction.Down;
}
else if (info.Key.Equals(ConsoleKey.LeftArrow) && movement != Direction.Right)
{
    movement = Direction.Left;
}
else if (info.Key.Equals(ConsoleKey.RightArrow) && movement != Direction.Left)
{
    movement = Direction.Right;
}

  • Привет .. У меня все еще много проблем с кодом.

    — MrLowBot

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *