最近,我一直在尝试学习SOLID原理。我通过批评中学到了最好的东西。

我用C#在Unity中制作了这个蛇游戏,在这里我测试了对SOLID原理的当前理解。游戏本身效果不错,但我正在尝试编写更好的代码。

主要问题:


我打破了哪些原则?我该如何解决?
我应该使用接口而不是多态吗?

其他:


我做任何不好的做法吗?
我的设计不好吗?

我不了解Unity,也无法阅读您的代码。

别担心,我在这个游戏中几乎没有使用Unity。一切都应该可以理解。

解释我的解决方案:

我的解决方案是将所有对象放置在“网格”中的位置。多个对象可以位于任何位置。在所有对象中的Before()Next()After()中处理逻辑。每0.1秒(或每步)调用一次。

每个对象都是独立的。每当用户按下按键时,都会放置方向对象。当蛇形部分在此方向对象的相同位置时,蛇形部分将获得该方向。方向obj一旦不再位于带有蛇形部分的位置,便被删除。

IVector2只是具有int xint y的类。它与接口无关(不好的命名,我的糟糕)。

自己的评论

对于这么小的游戏,文件的数量太可笑了。而且也花了一些时间。但是,错误非常容易找到和处理。喜欢它。

如果您喜欢什么,请随时使用。如果需要,我可以上传项目。如果需要,我还可以上传最终项目的视频。

文件名概述:


Game.cs:Monobehavior-启动在启动时被调用一次,每帧都会调用更新
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
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类br />AppleObj.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);
        }



    }
}


SnakePartObj.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);
        }
    }
}


BarrierObj.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));
        }
    }
}


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


其他

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


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

    }
}


评论

我的品味太多了。我是否有疑问,例如方向确实值得,因为它是自己的班级。另外,在称为Game的单个类中,CreateGame,DeleteGame之类的函数类似乎是更好的动作(函数)。

太多的代码让我无法阅读,但摆脱了所有的“ obj”前缀/后缀-一切都是对象,这不用多说;您无需在名称中包含该文本。

@ Vermacian55我发现您的问题还可以,并且没有太多介绍,相反,它对即将发生的事情有一个很好的概述。没错,有很多代码,但是我看到了更长的解释,而且还可以。

有17个类和600行代码。我会说这次可能真的太多了。我可能会问一些关于蛇的设计或评分系统等较小的问题。

这里的代码气味是“ design pattern run amok”。不管模式多么有用,对模式的严格遵守都会破坏该模式的价值。您已经有了Class Overflow,它使我想起了FizzBu​​zz Enterprise Edition。不要让可读性和YAGNI走出窗口。

#1 楼

只是几句话。回顾自下而上。



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)



不要!使用TKeyTValue

专用objectsHash似乎不是必需的。唯一使用它的地方是


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



您可以简单地使用SelectMany

 dictionary.Values.SelectMany(x => x)

/>


public class DirectionObj : Obj



所有内容似乎都是Obj或后缀-Obj。用OO语言调用所有obj就像您要调用所有事物一样。没有人会理解你。



public Direction dir { get; set; }



属性名称-> PascalCase。没有缩写。



dir.direction = Direction.names.Up;



代码缺乏一致性,但后缀Obj后缀:-)

评论


\ $ \ begingroup \ $
太好了!一堆我不知道的东西!谢谢。 .OfType似乎真的很方便。对象后缀<3
\ $ \ endgroup \ $
– filipot
17年1月6日在16:09



#2 楼

我感觉到您对SOLID的看法过多,padawon。您需要考虑什么是游戏,什么是游戏。对象做事。他们所做的决定了他们是什么。属性捕获事物在填充时的状态。将搜索留给面向对象设计的统一领域理论留待以后使用。
在一种方法的类中着迷于打开/关闭结果。
单一责任就是SOLID宇宙中的力量。您的中产阶级太低了,padawon。
在整个宇宙中,什么不是物体?将事物命名为“ obj”(或xxxObj)是为了否认其目的,padawon。
将Obj.cs发送给CERN,因为您已经发现了构成宇宙结构基础的一个基本粒子。它是一种无定形的抽象,通知从其派生的所有实体,它们没有目的。
安德森先生的秘密是,没有汤匙-苹果,屏障,或其他任何东西;不在这个蛇游戏世界的角落。
编辑-从下面复制注释

“我应该如何处理obj situasjon?您要重命名还是设计有缺陷? ”命名暗示设计可能存在缺陷。它向我暗示您正在考虑使用量子粒子术语,而不是“蛇形游戏”术语。我建议这是SRP故障开始的地方。通过对游戏的解释,确定各个部分,每个部分的功能以及它们之间的交互方式,仔细地谈论自己的方式。作为重复过程,您将确定特定的部分和常规的部分以及它们之间的关系

结束编辑

评论


\ $ \ begingroup \ $
我给你+1这个有趣的故事,我想他叫帕达万。
\ $ \ endgroup \ $
–t3chb0t
17年1月6日15:11

\ $ \ begingroup \ $
我有时会讲南方的口音。
\ $ \ endgroup \ $
– radarbob
17年1月6日在15:15

\ $ \ begingroup \ $
哈哈哈哈太好了。师父,我还有很多东西要学。当您说“您的Midiclorians太低”时,您是说我没有履行SRP吗?另外,我应该如何处理obj situasjon?您是要重命名还是设计有缺陷?如果有缺陷,我该如何解决?如果我将其重命名为“ DataBaseObject”,那不会有所作为吗?
\ $ \ endgroup \ $
– filipot
17年1月6日在16:15

\ $ \ begingroup \ $
当然可以。我可以做DataBase 而不是DataBase 。大声笑。
\ $ \ endgroup \ $
– filipot
17年1月6日在16:39

\ $ \ begingroup \ $
“我应该如何处理obj situasjon?您要重命名还是设计有缺陷?”命名暗示设计可能存在缺陷。它向我暗示您正在使用量子粒子术语而不是“蛇形游戏”术语进行思考。我建议这是SRP故障开始的地方。通过对游戏的解释,确定各个部分,每个部分的功能以及它们之间的交互方式,仔细地谈论自己的方式。作为重复过程,您将确定特定的部分和常规的部分,以及它们之间的关系。
\ $ \ endgroup \ $
– radarbob
17年1月6日在18:37

#3 楼

除了SOLID原则之外,请考虑您的类和方法。类/对象是存在的事物,方法是它们所做的事情。因此,在命名它们时,类名应为名词,方法应为动词。如果您发现自己在动词后面命名类,则表明您没有考虑这些类的实际用途。

这样,我在CreateGame,DeleteGame和GameOver类方面遇到了麻烦。创建和删除是您对Game对象所做的事情,而不是可以自己存在的事情。无论做什么,都应该是Game类中的方法,而不是自己的类。阅读代码,我在遵循您实例化CreateGame和DeleteGame对象的过程中遇到了麻烦。我认为,如果您将这些类重新考虑为在Game类上执行的方法,您还会发现代码变得更简单。更进一步,Game类可能不需要使用称为Create的方法。创建类的实例是构造函数的用途。

GameOver有点不同,名称实际上并没有描述名词或动词,而是描述了Game的状态(即是,游戏的状态已经结束)。考虑将其重写为Game类中名为End的方法,或者,因为如果我没看错代码,这一切似乎是在删除游戏,所以只需将其逻辑合并到Game.Delete方法中即可。同样,您在GamOver中拥有的ResetGame方法是您正在Game上执行的一个动作,应该属于Game类的一部分。

良好的通用规则:

Class =名词(是IS的东西)

Method =动词(是DOES的东西)

您可以使用的另一种技巧是用“我需要{方法} {类/对象}”之类的句子(例如“我需要开始游戏”或“我需要删除该SnakePart”)。然后,您知道该方法是否需要在该类上进行。同样,您可以将其应用于已经编写的代码,以查看是否有意义。如果您最终得到的句子像“我需要ResetGame the GameOver”那样没有意义,则可能是有些歪斜,现在正是修改您尝试在其中进行的好时机。

评论


\ $ \ begingroup \ $
是的,“ CreateGame”游戏和此类课程在我这方面显然是错误的。谢谢你的把戏!我可以看到它很有用。
\ $ \ endgroup \ $
– filipot
17年1月7日在19:25

#4 楼

总体设计首先是您的设计摘要:

Game类初始化游戏并运行主游戏循环(Update)。每一步,它都会应用用户输入并更新所有游戏对象(通过调用其BeforeNextAfter方法)。游戏对象(ApplyObjBarrierObjSnakePartObjDirectionObj类)位于2D上网格(由DataBase<IVector2, Obj>实例表示)。

玩家控制由多个部分(Snake)组成的蛇(SnakePartObj类)。处理箭头键时,网格上会放置一个不可见的方向标记。该标记告诉蛇形部分下一步应沿哪个方向移动。这样可以防止零件断开连接。

苹果会检查每个步骤是否与蛇形零件发生碰撞。如果是这样,他们告诉蛇增加其长度(这会增加玩家得分),然后他们将移动到随机的新位置。

障碍物还会检查每一步是否与蛇相撞。部分。如果是这样,它们就会触发游戏结束。

总体设计观察

上面的摘要中有一些突出的地方:


方向标记
苹果和障碍物不断检查蛇的零件
Before / Next / After方法
奇怪的数据库类

将蛇保持在一起可能是解决得更优雅。例如,通过去除尾巴部分并添加新的头部,因此其他部分根本不需要移动(当蛇长大时,您根本就不需要去除尾巴)。或通过为每个部分提供下一个部分的引用,因此它们不需要方向标记来告诉他们下一个要移动的位置。

为什么苹果和障碍物需要一直检查蛇形部位?蛇头总是会发生碰撞,因此明智的做法是只让蛇(头)检查碰撞。顺便说一句,您的代码是否考虑到了蛇可能会撞到自己?

具有3种不同的更新方法表明您遇到了订单问题。通过上述两个更改,您可能不再需要这三种方法。实际上,每个步骤一个Snake.Update调用就足够了-所有其他游戏对象仍然是被动的。

DataBase<key, obj>表示持久存储,但实际上用于表示包含游戏对象的2D网格。 Grid(或LevelMapEnvironment)将是更具描述性的名称。该类还仅使用IVector2作为键,而Obj作为值(obj),并且其方法名称在很大程度上暗示了二维运动,因此没有理由使它通用。 YAGNI:“您不需要它”。

类设计

进一步研究代码时,我们会看到创建了游戏对象(Obj) (并也已更新)通过工厂(ObjFactory),该工厂也在游戏网格(DataBase<IVector2, Obj>)中注册了它们。还有一个用于创建可视零件的工厂(GameObjectInstantiator)。

DataBase<key, obj>类使您可以访问所有游戏对象,并提供快速的空间查找以进行碰撞检查。它内部使用的是MultipleValuesDictionary<key, value>类。

还有一些与游戏状态相关的类,例如CreateGameDeleteGameGameOverCreateGame初始化玩家蛇并用障碍物和苹果填充游戏网格,而DeleteGame擦除游戏网格,而GameOver使用其他两个重新启动游戏。

然后是InputHandlerScoreDirectionIVector2类-实用程序。 InputHandler检查箭头按键是否按下并返回合适的方向,Score跟踪得分,Direction在各个位置(上,下,左或右)用作移动方向,IVector2存储2D坐标。

类设计观察

如果我们检查代码,则会注意到以下几点:




ObjFactory将对象创建与更新(游戏)混合在一起逻辑)
自定义(半)词典类
每当移动Obj时,游戏网格都必须保持最新状态
某些与游戏相关的功能已通过类而非方法实现了

Direction的设计很奇怪

ObjFactory的职责太多。那些CallAll*方法不属于那里。它的工作量也很小,所以我真的不相信工厂会在这里增加很多价值。此设计的一个缺点是,它使用new()约束,因此您不能使用构造函数来强制进行适当的初始化(直到调用SnakePartObj之前,InstallSnakePart尚未正确初始化,但是工厂不会为您这样做)。 >
不需要MultipleValuesDictionary<key, value>-本质上只是Dictionary<IVector2, HashSet<Obj>>。如果使用起来不方便,那么一些(扩展)帮助程序方法就足够了。您还可以使用HashSet<Obj>的2D数组。实际上,没有方向标记,您不需要每个像元支持多个对象,因此Obj的2D数组就足够了。

使对象位置与游戏网格保持同步显然会使事情复杂化。仅保留一个游戏对象列表可能会更容易-数量不多,并且您只需要检查一次碰撞(对于蛇的头部),因此不太可能遇到性能问题。或者,采用不同的蛇移动策略(删除/添加零件而不是移动零件),您根本不需要任何移动支撑。或者,您可以直接通过网格执行所有移动,因此游戏对象不需要那些需要游戏网格引用的方法。

其他人已经对您的CreateGameDeleteGameGameOver类进行了评论:应该是Game中的方法。

Direction是一个可变类,其中包含names枚举,并且包含一些实用方法。它的可变性允许使用一些不太直观的代码:InputHandler.HandleArrows通过更改其Direction参数来返回方向。当Direction本身是枚举时,它会更容易理解(更难于滥用),而InputHandler.HandleArrows只会返回Direction(枚举)。您可能需要阅读'(数据)封装'。

结论

关于实际的低级代码本身(遵循标准C#命名)还有很多要说的约定,使用更具描述性的方法和类名,而不是硬编码的值,以及其他各种低级别的改进),但是我认为这篇评论确实足够长。

总之,我认为您的代码执行起来过于复杂。 SOLID固然很好,但请不要忘记KISS:“保持简单,愚蠢”。 :)

评论


\ $ \ begingroup \ $
总体设计要点就可以了。 “那么,您的代码是否考虑到蛇可能会撞到自己?”,是的。在SnakePartObj.cs After函数中,它检查在同一位置是否有多个snakePartObj。如果是这样,那就是游戏结束。当一条蛇“咬”自己时,它将导致两条蛇在同一位置。是的,我遇到了订单问题,您的示例可以解决。我觉得我的设计给了我很大的自由度,但是当你说“ YAGNI”时再说一次。
\ $ \ endgroup \ $
– filipot
17年1月7日在19:39

\ $ \ begingroup \ $
不能完全理解“移动Obj时游戏网格必须保持最新状态”的意思,网格是一个类,因此具有引用权吗?因此它将自动保持最新状态,对吗?
\ $ \ endgroup \ $
– filipot
17年1月7日在19:40

\ $ \ begingroup \ $
“保持对象位置与游戏网格同步显然会使事情复杂化。仅保留游戏对象列表可能会更容易”。我编写代码的方式就像是一个大项目,以测试水域。但随后又出现“ YAGNI”
\ $ \ endgroup \ $
– filipot
17年1月7日在19:42

\ $ \ begingroup \ $
“ _此设计的一个缺点是它使用new()约束,因此您不能使用构造函数来强制进行正确的初始化(在调用InstallSnakePart之前,不能正确初始化SnakePartObj,但是工厂不会这样做”),您知道一种解决该问题的方法吗?
\ $ \ endgroup \ $
– filipot
17年1月7日19:43



\ $ \ begingroup \ $
是的,的确是KISS。谢谢!这为我清除了很多东西!
\ $ \ endgroup \ $
– filipot
17年1月7日在19:46