先上图片预览
需要的所有属性字段:
//背景色集
Color[] cls = new Color[] {Color.White,Color.AliceBlue,Color.Aqua,Color.Azure
,Color.Bisque,Color.Gray,Color.Red,Color.Coral,Color.Brown,Color.Yellow};
String[] images = new string[] {"1.png","2.png", "3.png", "4.png" ,"5.png", "6.png", "7.png", "8.png"
, "9.png" , "10.png" };
//随机数
Random r;
//地图大小
public int mapSize = 10;
//地图按钮集合
public Button[,] btns;
//记录上一个按钮
public static Button last_b = null;
//记录按钮对应坐标
public Hashtable BtoI;
//记录按钮是否已熄灭
public int[]map_index;
//划线点
public static Point p1;
public static Point p2;
//画图工具
Pen p;
Graphics g;
//得分窗口
public int score;
public TextBox text;
//计时器
ProgressBar pb;
在窗体加载时开始窗体控件的设计(已在设计面板上创建panel容器panel1用于放置各元素,个人认为使用panel的好处之一是可更方便定位元素放置位置):
private void Form2_Load(object sender, EventArgs e)
{
Font f = new Font("楷体", 20);
//label样式设计
Label score_l = new Label();
score_l.Height =50;
score_l.Width = 100;
score_l.Font = f;
score_l.Text = "得分:";
score_l.Location = new Point(panel1.Width + 200, 150);
//textbox样式设计
TextBox score_t = new TextBox();
text = score_t;
text.Text = score.ToString();
score_t.Location = new Point(panel1.Width+300,120);
score_t.Font = f;//字体
score_t.Height = 70;//高度
score_t.Width = 200;
score_t.Multiline = true;
this.Controls.Add(score_l);
this.Controls.Add(score_t);
this.Location = new Point(5, 5);
this.Width = 1200;
this.Height = 800;
textBox1.Hide();textBox2.Hide();textBox3.Hide();
panel1.Width = 750;
panel1.Height = 750;
//进度条样式设计
//pb = new ProgressBar();
//pb.Width = 500;pb.Height = 50;
//pb.Location = new Point(100, 0);//放置位置
//panel1.Controls.Add(pb);
//pb.Value = 100;//初始长度拉满
initialize();
}
以上设置了所需的各类自定义控件(可根据需求自行修改);
接下来设置最重要的按钮集及其各项属性:
private void initialize()
{
btns = new Button[mapSize, mapSize];
//timer1.Enabled = true;
//pb.Value = 100;
ArrayList arr = new ArrayList();
BtoI = new Hashtable();
for (int i=0;i<50;i++)
{
int x = r.Next(0, 10);
arr.Add(x);
arr.Add(x);
}
//初始化mapindex
int c = arr.Count;
map_index = new int[c];
while(--c>=0)
{
int i = r.Next(0, c + 1);
map_index[c] =(int) arr[i];
arr.RemoveAt(i);
}
//初始化按钮集坐标及大小
int w = panel1.Width;
int h = panel1.Height;
for (int i= 0;i<mapSize;i++)
{
for (int j = 0; j < mapSize; j++)
{
btns[i, j] = new Button();
btns[i, j].Width = btns[i, j].Height= w / 12;//设置大小
btns[i, j].Location = new Point(btns[i,j].Width+ j* (btns[i, j].Width+1) ,btns[i,j].Height+ i*(1+btns[i, j].Height));//设置坐标
panel1.Controls.Add(btns[i, j]);
//btns[i, j].Text = map_index[i * mapSize + j].ToString();
btns[i, j].BackgroundImage = Image.FromFile("Resources"+"\\"+images[map_index[i * mapSize + j]]);
btns[i, j].BackgroundImageLayout = ImageLayout.Zoom;
btns[i, j].BackColor = cls[map_index[i*mapSize+j]];
btns[i, j].Click += new EventHandler(this.Image_Click);//添加点击事件
Point p = new Point(i, j);
BtoI.Add(btns[i, j], p);
}
}
}
-由于每个按钮需要随机设置图案,map_Index数组用于存放该随机值。一个难点是如何设计成对的随机数字:
我这里的做法是先产生所需随机数的一半,另一半则通过第二次循环来填充。
-而我在这里具体使用到的的生成随机数的办法是:
Arraylist数组,其可根据需要插入和删除,并且内部的Count方法可以返回当前数组中元素个数。其类似一个可随机访问,但只可尾端插入的栈。
-原理是:每次生成一个随机数,将其拷贝一份一同插入Arraylist数组中,这样在结束插入时,数组中存放着两份完全相同的随机数序列。接下来每次生成一个随机数作为Arraylist的索引(前文提到其具随机访问和删除特性),将该索引处的数据作为按钮集
buttons的随机值,然后删除该索引处的数据。注意:此时Arraylist数组Count计数减一,相应的随机索引也需要对Count-1取模。以上实现成对的随机数。
-接下来对每个随机数自定义操作,可以针对每个索引使用不同的图片或背景色等等;可适当考虑需求。
对每个按钮添加点击事件:
private void Image_Click(object sender, EventArgs e)
{
var cur_btn = (Button)sender;
//初始
if (last_b == null)
{
last_b = cur_btn;
return;
}
//Point p_1 = new Point();Point p_2 = new Point();Point p_3 = new Point();Point p_4 = new Point();
var p_1 = (Point)BtoI[last_b];
var p_2 = (Point)BtoI[cur_btn];
var p_3 = btns[p_1.X, p_1.Y].Location;
var p_4 = btns[p_2.X, p_2.Y].Location;
var p_start = new Point(p_3.X + last_b.Width / 2, p_3.Y + last_b.Height / 2);
var p_end = new Point(p_4.X + last_b.Width / 2, p_4.Y + last_b.Height / 2);
if (last_b.BackColor != cur_btn.BackColor || !isConnected(last_b, cur_btn))
{
//MessageBox.Show("您有事吗?", "提示", MessageBoxButtons.YesNo, MessageBoxIcon.Information);
last_b = cur_btn;
}
else
{
last_b.Hide(); cur_btn.Hide();
//以下划线
g = panel1.CreateGraphics();
p = new Pen(Color.Red, 3);
MyDrawLine(g, p, p_1, p_2, p_start, p_end);
//以上划线
map_index[((Point)BtoI[last_b]).X * mapSize + ((Point)BtoI[last_b]).Y] = -1;
map_index[((Point)BtoI[cur_btn]).X * mapSize + ((Point)BtoI[cur_btn]).Y] = -1;
score += 100;
text.Text = score.ToString();
if (GameOver())
{
MessageBox.Show("通关成功√", "提示", MessageBoxButtons.OKCancel);
initialize();
}
last_b = null;
}
}
先梳理流程:
-用户开始游戏,此时没有任何点击动作。
-点击第一个方块,此时需要判断是否有已选中的方块:如果没有(如游戏开始阶段),记录当前的图案(其实也就是先前生成的随机数,记住随机的根源是前面产生的随机数,图案等为装饰,但也可作为判断标记);
而如果已有选中方块:判断两者图案是否相同:相同则消去,并且置空前一个按钮图案。(想象如果消除了一对图案,那么下一次点击是不会消除的)
不同则将当前图案置为对比图。
由于需要从点击的按钮返回寻找按钮对应的位置(将map_index置为访问过的标记等等),此处我的做法是在初始化按钮集时生成一个哈希表存储每个按钮对应位置。(由于还未十分了解C#哈希表部分内容,无法多作讲解)
在消除时增加了一个划线函数,即图示的消除红线。
private void MyDrawLine(Graphics g,Pen p, Point p_1, Point p_2, Point p_start,Point p_end)
{
if (IsStraightConnected(p_1.X, p_1.Y, p_2.X, p_2.Y))
{
g.DrawLine(p, p_start, p_end);
Delay_Show();
}
else if (IsLConnected(p_1.X, p_1.Y, p_2.X, p_2.Y))
{
var p_lx = new Point(btns[p1.X, p1.Y].Location.X + last_b.Width / 2,
btns[p1.X, p1.Y].Location.Y + last_b.Height / 2);
g.DrawLine(p, p_start, p_lx);
g.DrawLine(p, p_lx, p_end);
Delay_Show();
}
else if (IsZConnected(p_1.X, p_1.Y, p_2.X, p_2.Y))
{
var p_lx1 = new Point(btns[p1.X, p1.Y].Location.X + last_b.Width / 2,
btns[p1.X, p1.Y].Location.Y + last_b.Height / 2);
var p_lx2 = new Point(btns[p2.X, p2.Y].Location.X + last_b.Width / 2,
btns[p2.X, p2.Y].Location.Y + last_b.Height / 2);
g.DrawLine(p, p_start, p_lx1);
g.DrawLine(p, p_lx1, p_lx2);
g.DrawLine(p, p_lx2, p_end);
Delay_Show();
}
}
具体的画法也就是通过哈希寻找到按钮坐标,在其中心处向外划线。(注意,我曾在划线处遇到过问题,即在Form函数中使用drawline是无法看见画出的线的,与系统的渲染顺序有关,具体原因有兴趣的读者可以查阅资料)
-以及,Location方法获得的是图形左上角的坐标。需将其做一定调整才能变为中心处划线。
-由于设定划线为一闪而过,大致定个100ms:划线定时清除代码:
//定时删除划线
async public void Delay_Show()
{
await Task.Delay(100);
textBox1.Text = "..";
g = panel1.CreateGraphics();
g.Clear(this.BackColor);
}
用到async异步方法,不过合理只是简单调用了一个匿名task对象,完成简单的时延任务。
最后是判断按钮连线的逻辑代码:
//按钮判断条件
private bool isConnected(Button b1,Button b2)
{
int r1 = ((Point)BtoI[b1]).X;
int c1 = ((Point)BtoI[b1]).Y;
int r2 = ((Point)BtoI[b2]).X;
int c2 = ((Point)BtoI[b2]).Y;
if (IsStraightConnected(r1, c1, r2, c2))
{
textBox3.Text = "straight";
//g= btns[r1,c1]. CreateGraphics();
//p = new Pen(Color.Red);
//g.DrawLine(p,btns[r1,c1].Location,btns[r2,c2].Location);
//Delay_Show();
return true;
}
else if (IsLConnected(r1, c1, r2, c2))
{
textBox3.Text = "L";
return true;
}
else if (IsZConnected(r1, c1, r2, c2))
{
textBox3.Text = "Z";
return true;
}
return false;
}
//直接相连
private bool IsStraightConnected(int r1, int c1,int r2, int c2)
{
if (c1 == c2)
{
if (r2 < r1) { int t = r1; r1 = r2; r2 = t; }
for (int i = r1 + 1; i != mapSize; i++)
{
if (i == r2) return true;
else if (map_index[i * mapSize + c1] != -1) return false;
}
}
else if (r1 == r2)
{
if (c2 < c1) { int t = c1; c1 = c2; c2 = t; }
for (int j = c1 + 1; j != mapSize; j++)
{
if (j == c2) return true;
else if (map_index[r1*mapSize+j] != -1) return false;
}
}
return false;
}
//L型
private bool IsLConnected(int r1, int c1, int r2, int c2)
{
if (IsStraightConnected(r1, c2, r2, c2) && IsStraightConnected(r1, c2, r1, c1)
&& map_index[r1 * mapSize + c2] == -1)
{
p1 = new Point(r1, c2);
return true;
}
if (IsStraightConnected(r2, c1, r2, c2) && IsStraightConnected(r2, c1, r1, c1)
&& map_index[r2 * mapSize + c1] == -1)
{
p1 = new Point(r2, c1);
return true;
}
return false;
}
//Z型
private bool IsZConnected(int r1, int c1, int r2, int c2)
{
int direction = 1;
for (int count=0;count<2;count++)
{
direction = -direction;
//横向先行
for (int i = c1 + direction; (i>=0&&i<mapSize); i += direction)
{
if (IsLConnected(r1, i, r2, c2) && IsLConnected(r2, i, r1, c1))
{
p1 = new Point(r1, i);
p2 = new Point(r2, i);
return true;
}
}
}
for (int a=0;a<2;a++)
{
direction = -direction;
//纵向先行
for (int i = r1 + direction; (i >= 0 && i < mapSize); i += direction)
{
if (IsLConnected(i, c1, r2, c2) && IsLConnected(i, c2, r1, c1))
{
p1 = new Point( i,c1);
p2 = new Point(i,c2);
return true;
}
}
}
return false;
}
有一点迭代的意思:即直线连线的逻辑也适用于L型或Z型连接;L型即有一次转折,Z型为有两次转折。本处用到direction用于双向循环,从而避免对两个方向分别增加逻辑代码,造成冗余。
最后是游戏结束判断逻辑:
private bool GameOver()
{
for (int i=0;i<mapSize*mapSize;i++)
{
if (map_index[i] != -1)
return false;
}
return true;
}
即map_index全部被置为-1时,游戏结束。
本次设计也遇到了许多问题:1.我尝试使用ProgressBar进度条来限定一局游戏时间;并添加了Timer计时器,在time结束时调用initialize方法。但当计时结束但未消除完毕时,调用initialize方法后报错:按钮到索引的BtoI哈希表未设置引用到对象的实例。目前还 不知道如何解决。
2.还未实现地图外的连接,比如边界上两个图标可以通过边界外加的一行消除。思路大概是将map_index表向外扩展一圈,但具体实现还没能完成。
3.由于本次设计是三分钟热度,因而没有提前进行代码的设计,代码冗余十分严重,而且结构并不清晰,还望各位读者多多包涵。