Змеиная игра в C #

Недавно я пытался узнать принципы SOLID. Я узнаю лучшее через критику.

Я сделал эту игру змеи в Unity с C #, где я поставил свое текущее понимание принципов SOLID для тестирования. Сама игра работает замечательно, но я пытаюсь написать лучший код.

Основные вопросы:

  1. Какие принципы я нарушаю? И как я могу их исправить?
  2. Должен ли я использовать интерфейсы вместо полиморфизма?

Другое:

  1. Я делаю какие-то плохие практики?
  2. У меня плохой дизайн?

Я не знаю Unity и не могу прочитать ваш код

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

Объяснение моего решения:

Мое решение состоит в том, чтобы все объекты помещались в позиции в «сетке». Несколько объектов могут находиться в любых положениях. Логику обрабатывают в Before(), Next() и After() во всех объектах. Они вызывается каждые 0,1 секунды (или на шаг).

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

IVector2 - это просто класс с int x и int y. Это не имеет никакого отношения к интерфейсам (плохое имя, мое плохое).

Собственный комментарий

Количество файлов для этой маленькой игры нелепо. И это заняло некоторое время. Тем не менее, ошибки были НЕВЕРОЯТНЫМИ, легко найти и разобраться. Любил его.

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

Обзор имен файлов:

  • Game.cs: Monobehavior - Start вызывается один раз при запуске, Update вызывается каждый кадр
  • CreateGame.cs
  • DeleteGame.cs
  • GameOver.cs
  • InputHandler.cs
  • Score.cs
  • Snake.cs
  • ObjFactory.cs
  • Direction.cs
  • GameObjectInstantiator.cs

Objs

  • Obj.cs: базовый класс
  • VisualObj.cs: Obj
  • AppleObj.cs: VisualObj
  • SnakePartObj.cs: VisualObj - Каждая отдельная часть змеи
  • BarrierObj.cs: VisualObj - GameOver, если snakepart находится на барьере
  • DirectionObj.cs: Obj - изменяет направление snakePartObjs

Другое

  • DataBase.cs - Здесь объекты хранятся в этой «сетке»
  • MultipleValuesDictionary.cs - используется в базе данных

Game.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace SnakeGameSOLID
{
    public class Game : MonoBehaviour
    {
        public GameObject sphere;

        ObjFactory factory;
        InputHandler input;
        Snake snake;
        CreateGame createGame;
        GameOver gameOver;
        Score score;

        Direction inputDirection = new Direction();

        float timer = 1.0f;

        public void Start()
        {
            factory = new ObjFactory(sphere);
            input = new InputHandler();
            score = new Score();
            snake = new Snake(factory,score);
            createGame = new CreateGame();
            createGame.Create(snake, factory);
            gameOver = new GameOver(createGame, factory, snake);
            snake.InjectGameOver(gameOver);

        }

        public void Update()
        {
            timer -= Time.deltaTime;
            input.HandleArrows(inputDirection);
            if (timer < 0)
            {
                input.UseInput(factory, snake, inputDirection);
                timer = 0.1f;
                snake.Before();
                factory.CallAllBefore();
                factory.CallAllNext();
                factory.CallAllAfter();

                Debug.Log("The Current Score Is: " + score.GetScore().ToString());
            }
        }
    }
}

CreateGame.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Misc;
namespace SnakeGameSOLID
{
    public class CreateGame
    {

        public void Create(Snake snake, ObjFactory factory)
        {
            snake.CreateSnake(new IVector2(20,20),3);
            CreateEdgeBarriers(factory, 30, 30);
            //Five apples for fun
            for(int i = 0; i < 5; i ++)
                factory.CreateVisualObject<AppleObj>(new IVector2(Random.Range(1, 29), Random.Range(1, 29)), new Color(1, 0, 0, 1));
        }

        public void CreateEdgeBarriers(ObjFactory factory,int xSize, int ySize)
        {
            for (int x = 0; x < xSize; x++)
                for (int y = 0; y < ySize; y++)
                {
                    if (x == 0 || y == 0 || x == (xSize-1) || y == (ySize-1))
                        factory.CreateVisualObject<BarrierObj>(new IVector2(x, y), new Color(0,0,0,0));
                }
        }
    }
}

DeleteGame.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace SnakeGameSOLID
{
    public class DeleteGame 
    {

        public void Delete(ObjFactory factory)
        {
            factory.Clear();
        }
    }
}

GameOver.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace SnakeGameSOLID
{
    public class GameOver 
    {
        CreateGame createGame;
        DeleteGame deleteGame;
        ObjFactory factory;
        Snake snake;
        public GameOver(CreateGame createGame, ObjFactory factory, Snake snake)
        {
            this.snake = snake;
            this.factory = factory;
            this.createGame = createGame;
            deleteGame = new DeleteGame();
        }

        public void ResetGame()
        {
            deleteGame.Delete(factory);
            createGame.Create(snake, factory);
        }
    }
}

InputHandler.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace SnakeGameSOLID
{
    public class InputHandler
    {
        public void HandleArrows(Direction dir)
        {
            if (Input.GetKeyUp(KeyCode.UpArrow))
            {
                dir.direction = Direction.names.Up;
            }
            if (Input.GetKeyUp(KeyCode.DownArrow))
            {
                dir.direction = Direction.names.Down;
            }
            if (Input.GetKeyUp(KeyCode.LeftArrow))
            {
                dir.direction = Direction.names.Left;
            }
            if (Input.GetKeyUp(KeyCode.RightArrow))
            {
                dir.direction = Direction.names.Right;
            }
        }

        public void UseInput(ObjFactory factory,Snake snake, Direction dir)
        {
            if (dir.direction == Direction.names.None)
                return;

            //Dont use oppisite input
            if (snake.GetHeadDirection().IsOppisiteDirection(dir.direction))
            {
                return;
            }

            DirectionObj o = (DirectionObj)factory.CreateObject<DirectionObj>(snake.GetHeadPos());
            o.dir.direction = dir.direction;



            dir.direction = Direction.names.None;
        }

    }
}

Score.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace SnakeGameSOLID
{
    public class Score
    {
        int score = 0;
        public int GetScore() { return score; }
        public void AddPoint() { score++; }
        public void ResetScore() { score = 0; }
    }
}

Snake.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Misc;
namespace SnakeGameSOLID
{
    public class Snake 
    {
        SnakePartObj head;
        SnakePartObj lastPart;
        IVector2 lastPositionOfLastPart;
        Direction.names lastDirOfLastPart;
        ObjFactory objectfactory;
        Color headColor;
        Color snakeColor;
        GameOver gameOver;
        Score score;

        public Snake(ObjFactory factory,Score score)
        {
            objectfactory = factory;
            headColor = new Color(0, 1, 0, 1);
            snakeColor = new Color(0, 0.5f, 0, 1);
            this.score = score;
        }

        public void CreateSnake(IVector2 headPos, int NumbOfParts)
        {
            head = CreateSnakeObj(headPos, headColor);

            IVector2 pos = headPos;
            pos.y--;
            for(int y = 1; y < (NumbOfParts-1); y++)
            {
                CreateSnakeObj(pos, snakeColor);
                pos.y--;
            }

            lastPart = CreateSnakeObj(pos, snakeColor);
        }

        SnakePartObj CreateSnakeObj(IVector2 pos, Color col)
        {
            SnakePartObj obj = objectfactory.CreateVisualObject<SnakePartObj>(pos, col);
            obj.InstallSnakePart(this);
            return obj;
        }

        //Skeptical on this, breaks ISP?
        public void GameOver()
        {
            score.ResetScore();
            gameOver.ResetGame();
        }

        public void InjectGameOver(GameOver gameOver)
        {
            this.gameOver = gameOver;
        }

        public void CreateNewPart()
        {
            score.AddPoint();

            lastPart = CreateSnakeObj(lastPositionOfLastPart, snakeColor);
            lastPart.dir.direction = lastDirOfLastPart;

        }

        public void Before()
        {
            lastPositionOfLastPart = lastPart.position;
            lastDirOfLastPart = lastPart.dir.direction;
        }

        public IVector2 GetHeadPos()
        {
            return head.position;
        }

        public Direction GetHeadDirection()
        {
            return head.dir;
        }




    }
}

ObjFactory.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Linq;
using Misc;
namespace SnakeGameSOLID
{
    public class ObjFactory
    {
        GameObjectInstantiator instantiator;

        //Hold all of the positions of all objects, and the objects themselves
        DataBase<IVector2, Obj> objectDatabase;
        public ObjFactory(GameObject prefab)
        {
            instantiator = new GameObjectInstantiator(prefab);
            objectDatabase = new DataBase<IVector2, Obj>();
        }

        //Create object of type Obj and install
        public T CreateObject<T>(IVector2 position) where T : Obj, new()
        {
            T obj = new T();
            obj.Install(position);
            InsertObject(position, obj);
            return obj;
        }

        //Create object of Type ObjVisual and install
        public T CreateVisualObject<T>(IVector2 position, Color color) where T : VisualObj, new()
        {
            T obj = new T();
            obj.InstallVisual(instantiator.CreateInstance(), color);
            obj.Install(position);
            InsertObject(position, obj);
            return obj;
        }

        public void Clear()
        {
            foreach (var o in objectDatabase.GetAllObjects().ToList())
            {
                o.DestroyThis(objectDatabase);
            }
            objectDatabase = new DataBase<IVector2, Obj>();
        }

        void InsertObject(IVector2 position, Obj o)
        {
            objectDatabase.AddEntry(position, o);
        }


        //Better way of doing these three functions?
        public void CallAllNext()
        {
            foreach (var o in objectDatabase.GetAllObjects().ToList())
            {
                o.Next(objectDatabase);
            }
        }

        public void CallAllAfter()
        {
            foreach (var o in objectDatabase.GetAllObjects().ToList())
            {
                o.After(objectDatabase);
            }
        }
        public void CallAllBefore()
        {
            foreach (var o in objectDatabase.GetAllObjects().ToList())
            {
                o.Before(objectDatabase);
            }
        }
    }
}

Direction.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Misc;
namespace SnakeGameSOLID
{
    public class Direction
    {
        public enum names { Left, Right, Up, Down, None }
        public names direction { get; set; }

        public IVector2 GetVector()
        {
            switch (direction)
            {
                case names.Left:
                    return new IVector2(-1, 0);

                case names.Right:
                    return new IVector2(1, 0);
                case names.Up:
                    return new IVector2(0, 1);
                case names.Down:
                    return new IVector2(0, -1);
            }
            return new IVector2(0, 0);
        }

        public bool IsOppisiteDirection(names dir)
        {
            if(dir == names.Up && direction == names.Down)
            {
                return true;
            }

            if (dir == names.Down && direction == names.Up)
            {
                return true;
            }

            if (dir == names.Left && direction == names.Right)
            {
                return true;
            }

            if (dir == names.Right && direction == names.Left)
            {
                return true;
            }
            return false;
        }
    }
}

GameObjectInstantiator

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace SnakeGameSOLID
{
    class GameObjectInstantiator
    {
        GameObject prefab;
        public GameObjectInstantiator(GameObject prefab)
        {
            this.prefab = prefab;
        }
        public GameObject CreateInstance()
        {
            return (GameObject)GameObject.Instantiate(prefab);
        }
    }
}

классы Obj

Obj.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Misc;
namespace SnakeGameSOLID
{

    //Should i use interfaces instead? Combined? Or not?
    public class Obj
    {
        public IVector2 position { get; private set; }


        public Obj()
        {

        }
        public virtual void Install(IVector2 position)
        {
            SetPosition(position);
        }
        public virtual void Next(DataBase<IVector2,Obj> objects) {

        }

        public virtual void After(DataBase<IVector2, Obj> objects) { }
        public virtual void Before(DataBase<IVector2, Obj> objects) { }

        public void Move(DataBase<IVector2,Obj> objects, IVector2 newPos)
        {
            newPos += position;
            objects.MoveEntry(position, newPos, this);
            SetPosition(newPos);
        }

        public void Replace(DataBase<IVector2, Obj> objects, IVector2 newPos)
        {
            objects.MoveEntry(position, newPos, this);
            SetPosition(newPos);
        }

        protected virtual void SetPosition(IVector2 pos)
        {
            position = pos;

        }

        public virtual void DestroyThis(DataBase<IVector2, Obj> objects)
        {
            objects.RemoveEntry(position,this);
        }



    }
}

VisualObj.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Misc;
namespace SnakeGameSOLID
{
    public class VisualObj : Obj
    {
        GameObject instance;



        protected override void SetPosition(IVector2 pos)
        {

            base.SetPosition(pos);
            instance.transform.position = new Vector3(pos.x, pos.y, 0);
        }

        public void InstallVisual(GameObject instance, Color color)
        {
            this.instance = instance;
            //Just setting the color of the visual object
            instance.GetComponent<MeshRenderer>().material.color = color;
        }

        public override void DestroyThis(DataBase<IVector2, Obj> objects)
        {
            GameObject.Destroy(instance);
            base.DestroyThis(objects);
        }
    }
}

AppleObj.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Misc;
namespace SnakeGameSOLID
{
    public class AppleObj : VisualObj
    {
        public AppleObj() : base()
        {
        }

        public override void Next(DataBase<IVector2, Obj> objects)
        {

            SnakePartObj snakePart = objects.GetObjectOfType<SnakePartObj>(position);

            //If there is a snake part at the apples position, then make the snake longer and place the apple in another position
            if(snakePart != null)
            {
                snakePart.GetSnake().CreateNewPart();
                Replace(objects, RandomPos());
            }
        }

        IVector2 RandomPos()
        {
            return new IVector2(UnityEngine.Random.Range(1, 27), UnityEngine.Random.Range(1, 27));
        }
    }
}

SnakePartObj.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Misc;
namespace SnakeGameSOLID
{
    public class SnakePartObj : VisualObj
    {

        public Direction dir { get; set; }
        Direction directionChange = new Direction();
        Snake snake;
        public SnakePartObj() : base()
        {
            dir = new Direction();
            dir.direction = Direction.names.Up;
            directionChange.direction = Direction.names.Up;
        }

        public override void Next(DataBase<IVector2, Obj> objects)
        {
            base.Next(objects);
            Move(objects, dir.GetVector());
        }
        public override void After(DataBase<IVector2, Obj> objects)
        {


            var snakePartsAtPos = objects.GetObjectsOfType<SnakePartObj>(position);
            //There can only be maxinum one snake part at a position at all times
            if(snakePartsAtPos != null)
            if(snakePartsAtPos.Count > 1)
            {
                snake.GameOver();
            }
        }

        //Is this bad practice?
        public Snake GetSnake()
        {
            return snake;
        }

        public void InstallSnakePart(Snake snake)
        {
            this.snake = snake;
        }
    }
}

BarrierObj.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Misc;
namespace SnakeGameSOLID
{
    public class BarrierObj : VisualObj
    {
        public BarrierObj() : base()
        {
        }


        public override void After(DataBase<IVector2, Obj> objects)
        {

            SnakePartObj snakePart = (SnakePartObj)objects.GetObjectOfType<SnakePartObj>(position);
            if (snakePart != null)
            {
                snakePart.GetSnake().GameOver();
            }
        }
    }
}

DirectionObj.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Misc;
namespace SnakeGameSOLID
{
    public class DirectionObj : Obj
    {
        public Direction dir { get; set; }
        public DirectionObj() : base()
        {
            dir = new Direction();
            dir.direction = Direction.names.Up;
        }
        public Direction.names GetDirection() { return dir.direction; }

        public override void Before(DataBase<IVector2, Obj> objects)
        {
            base.Before(objects);

            SnakePartObj snakeObjAtPos = objects.GetObjectOfType<SnakePartObj>(position);

            //Change the direction of the snake part if it is on this directionObj
            ChangeSnakeDirection(snakeObjAtPos);
            //If there are no snake parts in this position, then delete this
            if (snakeObjAtPos == null)
                this.DestroyThis(objects);


        }

        void ChangeSnakeDirection(SnakePartObj part)
        {
            if (part == null)
                return;
            part.dir.direction = dir.direction;
        }

    }
}

Другое

DataBase.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System;

namespace SnakeGameSOLID
{
    public class DataBase <key,obj>
    {
        MultipleValuesDictionary<key,obj> objects = new MultipleValuesDictionary<key, obj>();
        HashSet<obj> objectsHash = new HashSet<obj>();

        public void AddEntry(key key, obj obj)
        {
            objects.AddEntry(key, obj);
            objectsHash.Add(obj);
        }

        public void RemoveEntry(key key, obj obj)
        {
            objects.RemoveEntry(key, obj);
            objectsHash.Remove(obj);
        }

        public void RemoveEntry(key key)
        {
            HashSet<obj> values = objects.GetValues(key);
            foreach(var val in values)
            {
                objectsHash.Remove(val);
            }

            objects.RemoveEntry(key);

        }

        public void MoveEntry(key oldKey,key newKey, obj obj)
        {
            RemoveEntry(oldKey, obj);
            AddEntry(newKey, obj);
        }

        public Dictionary<key,HashSet<obj>> GetAllEntries()
        {
            return objects.GetDictionary();
        }

        public HashSet<obj> GetAllObjects()
        {
            return objectsHash;
        }


        public T GetObjectOfType<T>(key k) where T : obj
        {
            var objs = GetObjectsOfType<T>(k);
            if (objs == null)
                return default(T);
            if (objs.Count == 0)
                return default(T);

            return (T)objs[0];

        }

        public List<obj> GetObjectsOfType<T>(key k) where T : obj
        {
            HashSet<obj> objs = objects.GetValues(k);
            if (objs == null)
                return null;

            return objs.Where(o => (o.GetType() == typeof(T))).ToList();

        }

        public List<obj> GetObjectsOfType<T>() where T: obj
        {
            return objectsHash.Where(o => (o.GetType() == typeof(T))).ToList();
        }


    }
}

MultipleValuesDictionary.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
namespace SnakeGameSOLID
{
    //Solution to having multiple values in a dictionary
    public class MultipleValuesDictionary<key, value>
    {
        Dictionary<key, HashSet<value>> dictionary = new Dictionary<key, HashSet<value>>();

        public void AddEntry(key k, value v)
        {
            HashSet<value> values;
            if (dictionary.TryGetValue(k, out values))
            {
                if (values.Contains(v))
                {
                    //Exception?
                    return;
                }
                values.Add(v);
                return;

            }
            values = new HashSet<value>();

            values.Add(v);
            dictionary.Add(k, values);
        }

        public void RemoveEntry(key k)
        {
            dictionary.Remove(k);
        }

        public void RemoveEntry(key k, value val)
        {
            HashSet<value> values = GetValues(k);
            values.Remove(val);
        }

        public Dictionary<key,HashSet<value>> GetDictionary()
        {
            return dictionary;
        }
        public HashSet<value> GetValues(key k)
        {

            HashSet<value> values;
            dictionary.TryGetValue(k, out values);
            return values;
        }

        //Gets all values of a specific type at a position
        public List<value> GetValuesOfTypeAtKey<T>(key k) where T : value
        {
            HashSet<value> values = GetValues(k);
            return values.Where(o => (o.GetType() == typeof(T))).ToList();
        }

    }
}
27 голосов | спросил Vermacian55 6 Jpm1000000pmFri, 06 Jan 2017 16:19:38 +030017 2017, 16:19:38

4 ответа


17

Несколько замечаний. Просмотр снизу вверх.


public class MultipleValuesDictionary<key, value>

Не называйте словарь классов, если он не один (реализация интерфейса IDictionary<,>). Это запутанно.

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

Не придумывайте новые имена общих параметров. Соглашения состоят в том, чтобы начинать имена с прописной буквы T.

Вы можете просто определить его как:

public class MultipleValuesDictionary<TKey, TValue> : Dictionary<TKey, HashSet<TValue>> { }

public List<value> GetValuesOfTypeAtKey<T>(key k) where T : value
{
    HashSet<value> values = GetValues(k);
    return values.Where(o => (o.GetType() == typeof(T))).ToList();
}

В словаре нет такого метода. Это особый случай, и вы можете легко получить то, что вам нужно, с расширением OfType<T>:

 var results = multipleValueDictionary[key].OfType<MyType>().ToList();

public class DataBase <key,obj>

Это интересный класс с еще более интересным соглашением об именах:

public void AddEntry(key key, obj obj)

Не надо! Используйте TKey и TValue.

Частный objectsHash не похоже, что это было необходимо. Единственное место, где вы его используете, -

public HashSet<obj> GetAllObjects()
{
    return objectsHash;
}

, где вы можете просто использовать SelectMany

 dictionary.Values.SelectMany(x => x)

public class DirectionObj : Obj

Все кажется либо Obj, либо имеет суффикс -Obj. Вызов всего obj на языке OO - это то, что вы все называете вещь. Никто вас не поймет.


public Direction dir { get; set; }

Имена свойств -> PascalCase. Нет сокращений.


dir.direction = Direction.names.Up;

В коде отсутствует согласованность, но суффикс Obj: -)

ответил t3chb0t 6 Jpm1000000pmFri, 06 Jan 2017 17:39:50 +030017 2017, 17:39:50
13

Я чувствую, что вы слишком много думаете о SOLID, padawon. Вам нужно подумать о том, что такое игра, и о том, что такое ARE. Объекты делают вещи. То, что они делают, определяет, каковы они. Свойства фиксируют состояние вещи, как она делает вещи. Оставьте поиск единой теории поля объектно-ориентированного дизайна для более поздних версий.

Одержимость Open /Close приводит к классам с одним методом.

Единая ответственность - это сила w /в SOLID. Ваши midiclorians слишком низки, padawon.

Что, во всей Вселенной, не является объектом? Назвать вещь «obj» (или xxxObj) - значит отрицать ее цель, padawon.

Отправьте Obj.cs в CERN, поскольку вы обнаружили одну, фундаментальную частицу, лежащую в основе ткани Вселенной. Это аморфная абстракция, которая информирует все сущности, полученные оттуда, что они не имеют никакой цели; что каждый obj - это различие без разницы.

Секрет: г-н Андерсон, что нет ложки - или Apple, или Барьера, или чего-то еще; не в этом Змеином Игровом углу Вселенной. Здесь нет ни одного целостного класса.
ответил radarbob 6 Jpm1000000pmFri, 06 Jan 2017 18:01:10 +030017 2017, 18:01:10
11

Помимо принципов SOLID, подумайте о своих классах и методах. Классы /объекты - это вещи, которые существуют, методы - это то, что они делают. Поэтому, при именовании, имена классов должны быть существительными, методы должны быть глаголами. Если вы обнаруживаете, что называете свои классы после глаголов, это означает, что вы не задумываетесь о том, на что эти классы действительно предназначены.

Как таковой, у меня проблемы с классами CreateGame, DeleteGame и GameOver. Создание и удаление - это то, что вы делаете с объектом Game, а не с вещами, которые могут существовать сами по себе. Все, что они делают, должно быть методами класса Game, а не классами для себя. Читая код, у меня возникают проблемы с тем, что вы пытаетесь сделать при создании объектов CreateGame и DeleteGame. Я думаю, что если вы переосмыслите эти классы на методы, которые вы выполняете в классе Game, вы также обнаружите, что ваш код станет проще. Кроме того, метод, называемый Create, вероятно, не нужен для класса Game. Создание экземпляра класса - это конструктор для.

GameOver немного отличается тем, что имя на самом деле не описывает существительное или глагол, а скорее состояние игры (то есть состояние закончившейся игры). Подумайте о том, чтобы переписать это как метод класса Game под названием End, или, поскольку он кажется, что все это делает, это удаление игры, если я читаю ваш код правильно, просто включите его логику в метод Game.Delete. Ваш метод ResetGame, который у вас есть в GamOver, - это снова действие, которое вы выполняете в игре, и должно быть частью класса Game.

Хорошее общее правило:

Класс = существительное (что-то такое)

Метод = глагол (что-то класс DOES)

Другим трюком, который вы можете использовать, является то, что вы пытаетесь выполнить с предложением типа «Мне нужно {method} {class /object}» (например, «Мне нужно начать игру» или «Мне нужно для удаления SnakePart "). Затем вы знаете, что это метод, который должен быть в этом классе. Аналогично, вы можете применить это к коду, который вы уже написали, чтобы узнать, имеет ли он смысл. Если вы закончите с предложениями, которые не имеют смысла, как «Мне нужно ResetGame GameOver», скорее всего, что-то пошло, и самое время пересмотреть то, что вы пытались там сделать.

ответил Seth R 6 Jpm1000000pmFri, 06 Jan 2017 21:06:13 +030017 2017, 21:06:13
5

Общая конструкция

Сначала сводка вашего дизайна:

Класс Game инициализирует игру и запускает основной цикл игры (Update). Каждый шаг, он применяет ввод пользователя и обновляет все игровые объекты (путем вызова методов Before, Next и After).

Объекты игры (классы ApplyObj, BarrierObj, SnakePartObj и DirectionObj) расположены на 2D-сетке (представлен экземпляром DataBase<IVector2, Obj>).

Игрок управляет змеей (класс Snake), который состоит из нескольких частей (SnakePartObj). При нажатии клавиши со стрелкой на сетке помещается маркер невидимого направления. Этот маркер указывает части змей, в каком направлении они должны двигаться дальше. Это предотвращает отсоединение деталей.

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

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

Общие проектные наблюдения

В приведенном выше сводке выделяется несколько вещей:

  • указатели направления
  • Яблоки и барьеры постоянно проверяют наличие частей змей.
  • Before /Next /After методов
  • класс нечетных баз данных

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

Почему яблоки и барьеры должны постоянно проверять змеиные части? Столкновения всегда происходят с головой змеи, поэтому разумно позволять только змее (голове) проверять наличие столкновений. Кстати, учитывает ли ваш код, что змея может поразить себя?

Имея 3 разных метода обновления, вы указываете, что у вас проблемы с порядком. С этими двумя изменениями вам больше не нужны эти 3 метода. Фактически, один вызов Snake.Update на один шаг должен быть достаточным - все остальные игровые объекты в любом случае пассивны.

DataBase<key, obj> подразумевает постоянное хранилище, но фактически используется для представления 2D-сетки, содержащей игровые объекты. Grid (или Level, Map, Environment)) будет более описательным именем. Класс также используется только с ключом IVector2 и Obj как значение (obj), а его имена методов сильно подразумевают 2D-движение, поэтому нет причин сделать его общим , ЯГНИ: «тебе это не понадобится».

Дизайн классов

Когда мы посмотрим дальше в код, мы увидим, что игровые объекты (Obj) создаются (а также обновляются) через фабрику (ObjFactory), которая также регистрирует их в игровой сетке (DataBase<IVector2, Obj>). Существует дополнительная фабрика для создания визуальных частей (GameObjectInstantiator).

Класс DataBase<key, obj> предоставляет вам доступ ко всем игровым объектам и обеспечивает быстрый пространственный поиск для проверки конфликтов. Он использует внутренний класс MultipleValuesDictionary<key, value>.

Существует также несколько связанных с игровым классами классов, таких как CreateGame, DeleteGame и GameOver. CreateGame инициализирует змею игрока и заполняет сетку игры барьерами и яблоками, а DeleteGame стирает сетку игр, а GameOver использует два других перезапустите игру.

Тогдаэто классы InputHandler, Score, Direction и IVector2 - утилиты, в основном. InputHandler проверяет нажатие клавиши со стрелкой и возвращает соответствующее направление, Score отслеживает счет, Direction используется в разных местах в качестве направления движения ( вверх, вниз, влево или вправо) и IVector2 хранит 2D-координату.

Наблюдения за классом

Если мы проверим код, отметим несколько вещей:

  • ObjFactory смешивает создание объекта с обновлением (логика игры)
  • пользовательский (полу) словарный класс
  • игровая сетка должна быть обновлена ​​при перемещении Obj
  • некоторые функции, связанные с игрой, были реализованы с использованием классов вместо методов
  • Direction нечетно спроектировано

ObjFactory имеет слишком много обязанностей. Те методы CallAll* там не принадлежат. Это также очень мало, поэтому я не уверен, что завод добавляет большую ценность здесь. Одним из недостатков этого проекта является то, что он использует ограничение new(), поэтому вы не можете использовать конструкторы для обеспечения правильной инициализации (SnakePartObj не инициализируется должным образом, пока вы не назовете InstallSnakePart, но фабрика не делает этого для вас).

Нет необходимости в InstallSnakePart - это, по сути, просто MultipleValuesDictionary<key, value>. Если это слишком неудобно использовать, то достаточно нескольких (дополнительных) вспомогательных методов. Вы также можете использовать 2D-массив Dictionary<IVector2, HashSet<Obj>>. На самом деле, без указателей направления вам не нужно поддерживать несколько объектов на ячейку, поэтому достаточно 2D-массива HashSet<Obj>.

Сохранение позиций объектов в синхронизации с сеткой игр явно усложняет ситуацию. Может быть, проще просто сохранить список игровых объектов - их не так много, и вам нужно только проверять наличие столкновений один раз, для головы змеи, поэтому маловероятно, что вы столкнетесь с проблемами производительности. Или, с другой стратегией движения змей (удаление /добавление частей вместо их перемещения), вам вообще не нужна поддержка движения. В качестве альтернативы вы можете выполнять все движения через сетку напрямую, поэтому игровым объектам не нужны те методы, для которых требуется ссылка сетки игр.

Другие уже прокомментировали ваши классы Obj, CreateGame и DeleteGame: это должны быть методы в GameOver .

Game - это изменяемый класс, который содержит перечисление Direction, и содержит некоторые полезные методы. Его изменяемая природа допускает некоторый довольно неинтуитивный код: names возвращает направление, изменяя его аргумент InputHandler.HandleArrows. Было бы гораздо легче понять (и сложнее использовать), когда Direction был перечислимым, а Direction просто вернул InputHandler.HandleArrows (перечисление). Возможно, вы захотите ознакомиться с инкапсуляцией «данных».

Заключение

Еще нужно сказать о самом самом низкоуровневом коде (придерживаясь стандартных соглашений об именах C #, используя более описательный метод и имена классов, а не значения жесткого кодирования, различные другие низкоуровневые улучшения), но я чувствую это обзор достаточно длинный, как есть.

В целом, я думаю, что ваш код слишком сложный для того, что он делает. SOLID - это хорошо и все, но не забывайте KISS: «Держите его простым, глупым». :)

ответил Pieter Witvoet 7 Jam1000000amSat, 07 Jan 2017 03:12:32 +030017 2017, 03:12:32

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132