本周的周作业是2048,遗憾是没有时间做AI。


总结分为: 教训、思路、代码三个部分。
一.教训
1.第一次做还是采用老的编程思想,去琢磨算法,做了一个改良版的冒泡排序来交换每个块的数字,但是忙活了一天以失败而告终,因为好不容易换到边界,又被后面的循环换回来了;
2.第二次做还是采用了3层for循环的结构,单独做把非零数字交换到目标方向的操作,然后就研究网上一个可以实现矩阵旋转的方法,简单的说就是先按一个方向做好算法,然后写一个方法把整个二维数组顺时针倒转到写好算法的方向,排序完再逆时针倒转回去,这是个巧妙的办法,但是自己没弄完全弄懂,也没写出来。
3.周五下午老师开始讲怎么用面向对象的思想做这个游戏,先分析整个游戏世界的组成,找出最基础的元素,然后将这些共性写成一个基类,然后其他的块都在这个基类上画自己的,16个块的数字交换也是面向对象的,给每一个块做好自己的移动方向,这个方法根据移动方向接受一个目标块,然后向目标块移动和判断。主函数里只写了输入4个方向后二维数组遍历的顺序,其他都是整理数据,就这么简单的做完了。
4.其实在自己第一次做的时候就想过用面向对象的思想,给每个块写方法,但是不知道如何下手。
5.罗列一下周末自己重写卡住的地方:写继承于基类的CELL类时想不清继承后重写Draw方法的逻辑;画完记分板和16个块后发现数字投影下一小块的底色是黑色和设置颜色突兀,其实是重写时没有将前景色设置成属性里的forecolor;写每个块的移动方法时,想不清为啥写2个方法,然后为何互相调用,以及和主函数4个遍历for循环的关系;写死亡判定时卡在if和if else,后面完成后测试发现没区别;写随机数产生的方法时卡在如果没有等于0的块但是有能合并块时跳不出循环;当前分数和最高分数的逻辑;输入方向的x,y与数组下标[0,1]的对应关系;最后就是外部引用的调色基类colorsetter的使用。
二.思路
1.先分析游戏的规律,写下来
2.然后从共同的基类开始一层一层写下来
3.重点是写单个块的移动逻辑,根据输入方向给出一个目标块,使得朝目标块移动,交换数字或合并,标记状态是否移动或合并过,然后用for循环按既定方向调用每个块的方法,例如如果按下的是向左,就从左至右遍历,目标块在左。
4.常见BUG:一行或一列合并次数超过1次;有移动或合并才能产生新的随机数;数字满屏且没死亡时无法移动;真正死亡时无法结束游戏;清屏console.clear()带来的闪屏问题。
三.代码
一共有4个类:Program,GameObject,Cell,Board,另外还引用了一个调色的类库ColorSetter
Program:
class Program
{
public static Cell[,] cells; //定义静态字段Cell数组
public static Board scoreBoard;//定义静态字段计分板
public static GameObject go;//定义静态字段画大背景
public static Random roll = new Random();//定义静态字段随机数
public static int randomNum;//随机数变量
public static int consoleX = GameObject.startPoint.x;//代替起始点
public static int consoleY = GameObject.startPoint.y;//代替起始点
public static bool isGameOver = false;//检测游戏是否结束
public static bool isNewRound = false;//检测游戏是否结束
static void Main(string[] args)
{
char input = ' ';
bool isRightInput = false;
int directionX = 0;//记录方向向量
int directionY = 0;//记录方向向量
Console.Title = "2048";
Console.WindowWidth = 56;
Console.WindowHeight = 26;
Console.CursorVisible = false;//隐藏光标
//初始化16个Cell
SetCells();
//起始为2个随机数
RandomNum();
RandomNum();
//画大背景
DrawBackGroud();
//画小背景
DrawSmallBackGroud();
//循环开始 ↓
while (!isGameOver)
{
//部分清理屏幕
ClearPartConsole();
//如果是重新开始,则给出2个开始数
if (isNewRound)
{
RandomNum();
RandomNum();
}
isNewRound = false;
//画历史最高分数
DrawRecordBoards();
//画计分板
DrawScoreBoard();
//画数字和背景格
DrawCells();
//检测输入,选择方向
#region 输入检测
input = Console.ReadKey(true).KeyChar;
while (!isRightInput)
{
switch (input)
{
case 'w': isRightInput = true; directionX = GameObject.up.x; directionY = GameObject.up.y; break;
case 's': isRightInput = true; directionX = GameObject.down.x; directionY = GameObject.down.y; break;
case 'a': isRightInput = true; directionX = GameObject.left.x; directionY = GameObject.left.y; break;
case 'd': isRightInput = true; directionX = GameObject.right.x; directionY = GameObject.right.y; break;
}
}
isRightInput = false;
#endregion
//根据方向移动
#region 根据方向移动
switch (input)
{
case 'w':
for (int x = 0; x < cells.GetLength(1); x++)
{
for (int y = 0; y < cells.GetLength(0); y++)
{
cells[y, x].SetTargetThenMove(directionX, directionY);
}
}
break;
case 's':
for (int x = 0; x < cells.GetLength(1); x++)
{
for (int y = cells.GetLength(0) - 1; y >= 0; y--)
{
cells[y, x].SetTargetThenMove(directionX, directionY);
}
}
break;
case 'a':
for (int y = 0; y < cells.GetLength(0); y++)
{
for (int x = 0; x < cells.GetLength(1); x++)
{
cells[y, x].SetTargetThenMove(directionX, directionY);
}
}
break;
case 'd':
for (int y = 0; y < cells.GetLength(0); y++)
{
for (int x = cells.GetLength(1) - 1; x >= 0; x--)
{
cells[y, x].SetTargetThenMove(directionX, directionY);
}
}
break;
}
#endregion
//移动了才能产生新的随机数一个
if (MoveCheck())
{
RandomNum();
DrawCells();
}
//重置每个Cell里的布尔值
ResetBool();
#region 游戏检测胜利和失败
//游戏失败检测
if (DetectGameOver())
{
ConsoleColor tempFC = Console.ForegroundColor;
ConsoleColor tempBC = Console.BackgroundColor;
ColorSetter.ForegroundColor = Color.FromArgb(222, 0, 0);//@@
ColorSetter.BackgroundColor = Color.FromArgb(211, 211, 211);//@@
Console.SetCursorPosition(0, 0);
Console.WriteLine("Game Over!");
ContinueOrQuit();
Console.ForegroundColor = tempFC;
Console.BackgroundColor = tempBC;
}
//游戏胜利检测
if (DetectWin())
{
ConsoleColor tempFC = Console.ForegroundColor;
ConsoleColor tempBC = Console.BackgroundColor;
ColorSetter.ForegroundColor = Color.FromArgb(17, 255, 17);//@@
ColorSetter.BackgroundColor = Color.FromArgb(211, 211, 211);//@@
Console.SetCursorPosition(0, 1);
Console.WriteLine("You win!");
ContinueOrQuit();
Console.ForegroundColor = tempFC;
Console.BackgroundColor = tempBC;
}
#endregion
}
//循环结束 ↑
}
//部分清理屏幕
public static void ClearPartConsole()
{
ConsoleColor tempFC = Console.ForegroundColor;
ConsoleColor tempBC = Console.BackgroundColor;
ColorSetter.BackgroundColor = Color.FromArgb(211, 211, 211);
Console.SetCursorPosition(0, 0);
for (int j = 0; j < 40; j++)
{
Console.Write(" ");
}
Console.ForegroundColor = tempFC;
Console.BackgroundColor = tempBC;
}
//询问继续游戏还是退出游戏?
public static void ContinueOrQuit()
{
ConsoleColor tempFC = Console.ForegroundColor;
ConsoleColor tempBC = Console.BackgroundColor;
ColorSetter.ForegroundColor = Color.FromArgb(0,0,0);//@@
ColorSetter.BackgroundColor = Color.FromArgb(211, 211, 211);//@@
if (Board.score>= Board.highestScore)
{
Board.highestScore = Board.score;//记录最高分数
}
Console.SetCursorPosition(14, 0);
Console.WriteLine("继续游戏请按1");
Console.SetCursorPosition(28, 0);
Console.WriteLine("退出游戏请按2");
bool isRightChoice = false;
while (!isRightChoice)
{
char choice = Console.ReadKey(true).KeyChar;
switch (choice)
{
case '1':
foreach (var item in cells)
{
item.num = 0;
Board.score = 0;
isRightChoice = true;
isNewRound = true;
}
break;
case '2':
isGameOver = true;
isRightChoice = true;
break;
}
}
Console.ForegroundColor = tempFC;
Console.BackgroundColor = tempBC;
}
//非零的数字检测他们是否能移动或合并,不能就不产生新随机数
public static bool MoveCheck()
{
bool allCheck = false;
foreach (var item in cells)
{
if (item.num!=0)
{
allCheck = allCheck || item.IsMoved;
}
}
return allCheck;
}
//检测是否胜利
public static bool DetectWin()
{
bool has2048 = false;
foreach (var item in cells)
{
if (item.num==2048)
{
has2048 = true;
}
}
return has2048;
}
//检测是否游戏结束
public static bool DetectGameOver()
{
bool isAllFixed = true;
foreach (var item in cells)
{
isAllFixed = isAllFixed && item.TestFourFixed();
}
return isAllFixed;
}
//重置每个Cell里的2个布尔值
public static void ResetBool()
{
foreach (var item in cells)
{
item.ResetBool();
}
}
//产生随机数
public static void RandomNum()
{
bool isOK = false;
while (!isOK)
{
int count = 0;
foreach (var item in cells)
{
if (item.num == 0)
{
randomNum = roll.Next(1, 101);
if (randomNum < 40)
{
count++;
item.num = 2;
isOK = true;
break;
}
else if (randomNum < 45)
{
count++;
item.num = 4;
isOK = true;
break;
}
}
else
{
count++;
}
}
if (count==16)//排除当没有为0的空位,且有合并的相邻数字时,无法跳出这个循环去合并
{
break;
}
}
}
//画计分板
public static void DrawScoreBoard()
{
scoreBoard = new Board(consoleX, consoleY + 18, 4, 4, Color.FromArgb(255, 255, 255), Color.FromArgb(50, 50, 50));//@@@@标记调色
scoreBoard.display = "分数";
Board.score = 0;
foreach (var item in cells)
{
Board.score += item.num;
}
scoreBoard.Draw();
}
//画最高分记录板
public static void DrawRecordBoards()
{
scoreBoard = new Board(consoleX + 9, consoleY + 18, 6, 4, Color.FromArgb(255, 255, 255), Color.FromArgb(50, 50, 50));//@@@@标记调色
scoreBoard.display = "最高分数";
scoreBoard.Draw();
}
//画小背景板
public static void DrawSmallBackGroud()
{
go = new GameObject(consoleX - 1, consoleY - 1, 17, 17, Color.FromArgb(255, 255, 255), Color.FromArgb(180, 180, 180));//@@@@
go.Draw();
}
//画大背景板
public static void DrawBackGroud()
{
go = new GameObject(consoleX - 6, consoleY - 3, 28, 26, Color.FromArgb(255,255,255), Color.FromArgb(211,211,211));//@@@@
go.Draw();
}
//设置16个Cell的参数
public static void SetCells()
{
cells = new Cell[4, 4];
//初始化数组中每个cell,设置光标起始点
for (int y = 0; y < cells.GetLength(0); y++)
{
for (int x = 0; x < cells.GetLength(1); x++)
{
cells[y, x] = new Cell(consoleX + 4 * x, consoleY + 4 * y, 3, 3, 0);//这里的颜色在Draw里根据数字设置
cells[y, x].arrayX = x;//记录每个Cell的数组下标(1)
cells[y, x].arrayY = y;//数组下标(0)
}
}
}
//画16个Cell
public static void DrawCells()
{
foreach (var item in cells)
{
item.Draw();//这里会根据数字设置cell背景色和num背景色,前景色未改过都是默认颜色
}
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
GameObject:
struct Vector2
{
public int x;
public int y;
public Vector2(int newX, int newY)
{
x = newX;
y = newY;
}
}
class GameObject
{
protected Vector2 consolePos;
protected Vector2 size;
protected Color foreColor;//@@
protected Color backColor;//@@
public static Vector2 left = new Vector2(-1, 0);//(x,y)与数组位置下标相反[y,x]
public static Vector2 right = new Vector2(1, 0);
public static Vector2 up = new Vector2(0, -1);
public static Vector2 down = new Vector2(0, 1);
public static Vector2 startPoint=new Vector2(6,3);//定义一个唯一的起始点
public GameObject() { }
//下面这个给Cell用,默认前景色为白色,背景色为接近黑色
public GameObject(int ConsolePosX, int ConsolePosY, int SizeX, int SizeY) : this(ConsolePosX, ConsolePosY, SizeX, SizeY, Color.FromArgb(255, 255, 255), Color.FromArgb(50, 50, 50))//@@
{
}
//下面这个给其他显示板用
public GameObject(int ConsolePosX, int ConsolePosY, int SizeX, int SizeY, Color ForeColor, Color BackColor)//@@
{
consolePos.x = ConsolePosX;
consolePos.y = ConsolePosY;
size.x = SizeX;
size.y = SizeY;
this.foreColor = ForeColor;
this.backColor = BackColor;
}
public virtual void Draw()
{
//备份颜色
ConsoleColor tempFC = Console.ForegroundColor;
ConsoleColor tempBC = Console.BackgroundColor;
//设置颜色
ColorSetter.ForegroundColor = foreColor;//@@
ColorSetter.BackgroundColor = backColor;//@@
//设置光标起始点
//Console.SetCursorPosition(ConsolePos.x * 2, ConsolePos.y);
//根据size的大小画出来
for (int i = 0; i < size.y; i++)
{
Console.SetCursorPosition(consolePos.x * 2, consolePos.y+i);//每画一行,y+1
for (int j = 0; j < size.x; j++)
{
Console.Write(" ");
}
}
//还原颜色
Console.ForegroundColor = tempFC;
Console.BackgroundColor = tempBC;
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Cell:
class Cell : GameObject
{
public int num; //每个Cell的数字
public int arrayX;//数组下标(1)
public int arrayY;//数组下标(0)
public bool IsMerged = false;
public bool IsMoved = false;
public int tempX = 0;
public int tempY = 0;
public Cell(int ConsolePosX, int ConsolePosY, int SizeX, int SizeY, int newNum) : base(ConsolePosX, ConsolePosY, SizeX, SizeY)
{
num = newNum;
}
public void ResetBool()
{
IsMoved = false;
IsMerged = false;
}
public void MoveToTarget(Cell targetCell)//移动到目标cell
{
if (targetCell.num == 0)
{
targetCell.num = this.num;
this.num = 0;
this.IsMoved = true; //将当前Cell的移动设置为true
targetCell.IsMoved = true;//将目标Cell的移动设置为true
targetCell.SetTargetThenMove(tempX, tempY);
}
else if (targetCell.num == this.num && !targetCell.IsMerged && !this.IsMerged)
{
targetCell.num += this.num;
this.num = 0;
targetCell.IsMerged = true;//将目标Cell的合并设置为true
IsMerged = true;//将当前Cell的合并设置为true
this.IsMoved = true;//将当前Cell的移动设置为true
targetCell.IsMoved = true;//将目标Cell的移动设置为true
targetCell.SetTargetThenMove(tempX, tempY);//一直调用到达到边界为止
}
}
public void SetTargetThenMove(int directionX, int directionY)//根据方向给出目标Cell
{
if (arrayX + directionX < 0 || arrayX + directionX > 3 || arrayY + directionY < 0 || arrayY + directionY > 3)//越界判断
{
return;
}
else
{
tempX = directionX;//记录输入的方向
tempY = directionY;
MoveToTarget(Program.cells[arrayY + directionY, arrayX + directionX]);//数组下标位置与x,y相反,调用移动到目标的方法
}
}
public bool TestFourFixed()//检测四个方向这个块是否能动?
{
bool isFixed = TestOneFixed(GameObject.up) && TestOneFixed(GameObject.down) && TestOneFixed(GameObject.left) && TestOneFixed(GameObject.right);
return isFixed;
}
public bool TestOneFixed(Vector2 direction)//检测一个方向这个块是否能动?
{
if (arrayX + direction.x < 0 || arrayX + direction.x > 3 || arrayY + direction.y < 0 || arrayY + direction.y > 3)
{
return true;
}
if (Program.cells[arrayY + direction.y, arrayX + direction.x].num == 0 || Program.cells[arrayY + direction.y, arrayX + direction.x].num == this.num)
{
return false;
}
return true;
}
public override void Draw()//重写基类go里的Draw
{
//根据数字设置背景的颜色
SetColor();
base.Draw();
//备份颜色
ConsoleColor tempFC = Console.ForegroundColor;
ConsoleColor tempBC = Console.BackgroundColor;
//设置数字的打印位置
Console.SetCursorPosition((consolePos.x + size.x / 2 + 1) * 2 - num.ToString().Length, consolePos.y + size.y / 2);//size.x/2之所以加1是因为3/2=1
//设置数字的前后颜色,背景会根据数字改变,但是前景色未设置是默认的
ColorSetter.ForegroundColor = foreColor;//@@
ColorSetter.BackgroundColor = backColor;//@@
//画出数字
Console.Write(num);
//还原颜色
Console.ForegroundColor = tempFC;
Console.BackgroundColor = tempBC;
}
public void SetColor()//根据num设置每个Cell的颜色
{
switch (num.ToString())
{
case "0":backColor = Color.FromArgb(210, 210, 210); foreColor = Color.FromArgb(210, 210, 210); break;//浅灰
case "2": backColor = Color.FromArgb(255, 255, 255); foreColor = Color.FromArgb(0, 0, 0); break;//米白
case "4": backColor = Color.FromArgb(255, 255, 201); foreColor = Color.FromArgb(0, 0, 0); break;//浅黄
case "8": backColor = Color.FromArgb(255, 178, 101); foreColor = Color.FromArgb(0, 0, 0); break;//浅橙
case "16": backColor = Color.FromArgb(255, 128, 1); foreColor = Color.FromArgb(0, 0, 0); break;//深橙
case "32": backColor = Color.FromArgb(255, 67, 67); foreColor = Color.FromArgb(0, 0, 0); break;//浅红
case "64": backColor = Color.FromArgb(235, 0, 0); foreColor = Color.FromArgb(0, 0, 0); break;//深红
case "128": backColor = Color.FromArgb(255, 255, 45); foreColor = Color.FromArgb(0, 0, 0); break;//浅黄
case "256": backColor = Color.FromArgb(245, 238, 0); foreColor = Color.FromArgb(0, 0, 0); break;//黄
case "512": backColor = Color.FromArgb(137, 196, 255); foreColor = Color.FromArgb(0, 0, 0); break;//浅蓝
case "1024": backColor = Color.FromArgb(0, 119, 238); foreColor = Color.FromArgb(0, 0, 0); break;//深蓝
case "2048": backColor = Color.FromArgb(113, 255, 113); foreColor = Color.FromArgb(0, 0, 0); break;//绿色
}
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Board:
class Board:GameObject
{
public static int score;//单次分数,用一个静态变量
public static int highestScore;//历史最高分数,用一个静态变量
public string display;//文字显示变量
public Board(){}
public Board(int ConsolePosX, int ConsolePosY, int SizeX, int SizeY, Color ForeColor, Color BackColor) :base(ConsolePosX, ConsolePosY, SizeX, SizeY, ForeColor, BackColor)
{
}
public override void Draw()
{
base.Draw();
//备份颜色
ConsoleColor tempFC = Console.ForegroundColor;
ConsoleColor tempBC = Console.BackgroundColor;
//实例化的时候会设置分数和文字的前景和背景颜色
if ((size.x+size.y)<10)
{
ColorSetter.ForegroundColor = foreColor;//@@
ColorSetter.BackgroundColor = backColor;//@@
//设置文字的打印位置
Console.SetCursorPosition((consolePos.x + size.x / 2) * 2 - display.ToString().Length, consolePos.y + 1);//居中@@ //打印文字
Console.Write(display);
//设置当前分数的打印位置
Console.SetCursorPosition((consolePos.x + size.x / 2) * 2 - score.ToString().Length+1, consolePos.y + 2);//居中@@ //打印分数
Console.Write(score);
}
else
{
ColorSetter.ForegroundColor = foreColor;//@@
ColorSetter.BackgroundColor = backColor;//@@
//设置文字的打印位置
Console.SetCursorPosition((consolePos.x + size.x / 2) * 2 - display.ToString().Length, consolePos.y + 1);//居中@@ //打印文字
Console.Write(display);
//设置最高分数的打印位置
Console.SetCursorPosition((consolePos.x + size.x / 2) * 2 - highestScore.ToString().Length+1, consolePos.y + 2);//居中@@ //打印分数
Console.Write(highestScore);
}
//还原颜色
Console.ForegroundColor = tempFC;
Console.BackgroundColor = tempBC;
}
}
本文介绍了使用C#和Unity开发2048游戏的历程,包括采用面向对象编程思想重构游戏的过程。作者分享了从传统编程思维到面向对象思维的转变,以及在此过程中遇到的问题和解决方案,如矩阵旋转、游戏规则分析、单个块的移动逻辑等。文章以代码示例展示游戏的主要类和方法,帮助读者理解面向对象设计在游戏开发中的应用。

4130

被折叠的 条评论
为什么被折叠?



