我用C#在Unity中制作了这个蛇游戏,在这里我测试了对SOLID原理的当前理解。游戏本身效果不错,但我正在尝试编写更好的代码。
主要问题:
我打破了哪些原则?我该如何解决?
我应该使用接口而不是多态吗?
其他:
我做任何不好的做法吗?
我的设计不好吗?
我不了解Unity,也无法阅读您的代码。
别担心,我在这个游戏中几乎没有使用Unity。一切都应该可以理解。
解释我的解决方案:
我的解决方案是将所有对象放置在“网格”中的位置。多个对象可以位于任何位置。在所有对象中的
Before()
,Next()
和After()
中处理逻辑。每0.1秒(或每步)调用一次。每个对象都是独立的。每当用户按下按键时,都会放置方向对象。当蛇形部分在此方向对象的相同位置时,蛇形部分将获得该方向。方向obj一旦不再位于带有蛇形部分的位置,便被删除。
IVector2
只是具有int x
和int 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;
}
}
}
#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)
不要!使用
TKey
和TValue
。专用
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
\ $ \ 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
)。每一步,它都会应用用户输入并更新所有游戏对象(通过调用其Before
,Next
和After
方法)。游戏对象(ApplyObj
,BarrierObj
,SnakePartObj
和DirectionObj
类)位于2D上网格(由DataBase<IVector2, Obj>
实例表示)。玩家控制由多个部分(
Snake
)组成的蛇(SnakePartObj
类)。处理箭头键时,网格上会放置一个不可见的方向标记。该标记告诉蛇形部分下一步应沿哪个方向移动。这样可以防止零件断开连接。苹果会检查每个步骤是否与蛇形零件发生碰撞。如果是这样,他们告诉蛇增加其长度(这会增加玩家得分),然后他们将移动到随机的新位置。
障碍物还会检查每一步是否与蛇相撞。部分。如果是这样,它们就会触发游戏结束。
总体设计观察
上面的摘要中有一些突出的地方:
方向标记
苹果和障碍物不断检查蛇的零件
Before
/ Next
/ After
方法奇怪的数据库类
将蛇保持在一起可能是解决得更优雅。例如,通过去除尾巴部分并添加新的头部,因此其他部分根本不需要移动(当蛇长大时,您根本就不需要去除尾巴)。或通过为每个部分提供下一个部分的引用,因此它们不需要方向标记来告诉他们下一个要移动的位置。
为什么苹果和障碍物需要一直检查蛇形部位?蛇头总是会发生碰撞,因此明智的做法是只让蛇(头)检查碰撞。顺便说一句,您的代码是否考虑到了蛇可能会撞到自己?
具有3种不同的更新方法表明您遇到了订单问题。通过上述两个更改,您可能不再需要这三种方法。实际上,每个步骤一个
Snake.Update
调用就足够了-所有其他游戏对象仍然是被动的。DataBase<key, obj>
表示持久存储,但实际上用于表示包含游戏对象的2D网格。 Grid
(或Level
,Map
,Environment
)将是更具描述性的名称。该类还仅使用IVector2
作为键,而Obj
作为值(obj),并且其方法名称在很大程度上暗示了二维运动,因此没有理由使它通用。 YAGNI:“您不需要它”。类设计
进一步研究代码时,我们会看到创建了游戏对象(
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
尚未正确初始化,但是工厂不会为您这样做)。 > 不需要
MultipleValuesDictionary<key, value>
-本质上只是Dictionary<IVector2, HashSet<Obj>>
。如果使用起来不方便,那么一些(扩展)帮助程序方法就足够了。您还可以使用HashSet<Obj>
的2D数组。实际上,没有方向标记,您不需要每个像元支持多个对象,因此Obj
的2D数组就足够了。使对象位置与游戏网格保持同步显然会使事情复杂化。仅保留一个游戏对象列表可能会更容易-数量不多,并且您只需要检查一次碰撞(对于蛇的头部),因此不太可能遇到性能问题。或者,采用不同的蛇移动策略(删除/添加零件而不是移动零件),您根本不需要任何移动支撑。或者,您可以直接通过网格执行所有移动,因此游戏对象不需要那些需要游戏网格引用的方法。
其他人已经对您的
CreateGame
,DeleteGame
和GameOver
类进行了评论:应该是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
评论
我的品味太多了。我是否有疑问,例如方向确实值得,因为它是自己的班级。另外,在称为Game的单个类中,CreateGame,DeleteGame之类的函数类似乎是更好的动作(函数)。太多的代码让我无法阅读,但摆脱了所有的“ obj”前缀/后缀-一切都是对象,这不用多说;您无需在名称中包含该文本。
@ Vermacian55我发现您的问题还可以,并且没有太多介绍,相反,它对即将发生的事情有一个很好的概述。没错,有很多代码,但是我看到了更长的解释,而且还可以。
有17个类和600行代码。我会说这次可能真的太多了。我可能会问一些关于蛇的设计或评分系统等较小的问题。
这里的代码气味是“ design pattern run amok”。不管模式多么有用,对模式的严格遵守都会破坏该模式的价值。您已经有了Class Overflow,它使我想起了FizzBuzz Enterprise Edition。不要让可读性和YAGNI走出窗口。