您的位置:首页 > 其它

三阶魔方还原程序心得

2016-03-24 17:41 381 查看
第一次写技术性的博客啊,本人菜鸟,如果哪里说的不够准确完善,欢迎大神前来点拨呀~


先放几张效果图吧~







这是我最近做的一个三阶魔方还原的小程序。

用的环境是VC++6.0,其中也大量运用了easyx库进行图形绘制等。

这次写程序深刻感受到了算法是程序的灵魂。当时写魔方算法的时候,准备了两种算法,:第一种,储存每一步的步骤,再逆序还原,这方法实现起来到不太难;第二种,想用传统层先法还原,这倒是真把我难倒了。本来想着魔方还原都是有公式的,还原起来用这现成公式的算法不就好了吗?但是,写起来才发现没那么简单,因为人脑进行判断时可以迅速处理所看到的色块信息,灵活的进行公式变换,而电脑却不像人那样思考。可能对人来说,层先法第一步,底层十字还原,不难做到,因为实现的方法多种多样,但正是这多种多样,让电脑判断时也需逐条分析。我当初一直想寻找复原十字通法而无果(本人其实是魔方菜鸟......),后来在某魔方大神的指导下依次判断每个棱块进行检测,遍历96种情况,或许方法不是最简捷的,但终于完成了底层十字的复原,这一步的完成可谓是历史性的啊,正所谓万事开头难嘛,完成了十字,之后依次是底层角块、侧棱、顶层棱块色相、顶层角块色相、顶层角块位置、顶层棱块位置,这些步骤结合魔方公式到比十字得心应手得多了。运用魔方公式的算法具体实现方法就不赘述了,就是根据魔方公式与程序有机结合啦。

首先,魔方的面是用的二维数组,并且运用了C++的类。

class Face
{
public:
Face();
COLORREF color;
void coordinate(int x1, int y1, int x2, int y2);
void coloring();

private:
int iX1, iY1;      //立方体小块涂色坐标
int iX2, iY2;      //展开图小块涂色坐标
};

Face::Face()
{
color =  RGB(0, 250, 0);              //初始化
}

void Face::coordinate(int x1, int y1, int x2, int y2)
{
iX1 = x1;
iY1 = y1;
iX2 = x2;
iY2 = y2;
}

void Face::coloring()
{
setfillstyle(0);
setfillcolor(color);
floodfill(iX1, iY1, RGB(233, 233, 233));
floodfill(iX2, iY2, RGB(233, 233, 233));
}


实例化了六个面:

Face F[3][3];
Face U[3][3];
Face R[3][3];
Face L[3][3];
Face D[3][3];
Face B[3][3];


接下来说说图形界面。首先是一个发光魔方的欢迎界面,点击“Rubik”(魔方)后即可进入,进入后分为图形显示界面和步骤显示操作界面。图形显示界面左下角有操作说明:

“你可以摁字母控制魔方转动:大写代表顺时针,小写代表逆时针。如:R r U u L l D d F f B b M m X x Y y Z z 按数字0可退出,按*逆序还原,按+传统还原”。

操作说明框具体实现代码如下:

//提示框
setcolor(RGB(255, 174, 201));
setbkmode(TRANSPARENT);      //字体透明背景
setlinestyle(PS_SOLID, 10);
roundrect(50, 300, 450, 480, 50, 50);     //圆角矩形
setcolor(RGB(192, 45, 204));
LOGFONT f;
gettextstyle(&f);                     // 获取当前字体设置
f.lfHeight = 30;                      // 设置字体高度为 30
<span style="color:#cc33cc;background-color: rgb(255, 255, 153);">_tcscpy(f.lfFaceName, _T("Ravie"));    // 设置字体为“Ravie”
f.lfQuality = ANTIALIASED_QUALITY;    // 设置输出效果为抗锯齿
settextstyle(&f);                     // 设置字体样式
outtextxy(140, 310, "Rubik's Cube");
//提示框说明
_tcscpy(f.lfFaceName, _T("华文行楷"));
setcolor(RGB(184, 39, 254));
f.lfHeight = 20;
settextstyle(&f);
outtextxy(60, 340, "你可以摁字母控制魔方转动:");
outtextxy(60, 390, "大写代表顺时针,小写代表逆时针。");
//绿字提示
_tcscpy(f.lfFaceName, _T("jokerman"));
setcolor(GREEN);
f.lfHeight = 30;
settextstyle(&f);
outtextxy(60, 360, "如:R r U u L l D d F f B b M m X x Y y Z z");
outtextxy(60, 410, "按数字0可退出,按*逆序还原,按+传统还原");
coloring();
_tcscpy(f.lfFaceName, _T("jokerman"));
setcolor(RGB(253, 2, 46));
settextstyle(&f);


那些漂亮的字体,事实上windows已经为我们准备了很多漂亮的字体了哦!你只要打开“C:\Windows\Fonts”就能找到了哦~(*^__^*) 嘻嘻~

图形显示界面的背景是彩虹渐变色的背景,所用的方法是不断地画横线,从上到下填满整个屏幕,而渐变效果的秘诀就在于线条颜色参数的变化。代码如下:

// 画渐变的背景//彩虹背景色
float h = 0.0;		// 色相
float s = 1;		// 饱和度
float l = 0.7f;		// 亮度
for(int y = 0; y < 500; y++)
{
h += 0.72;
l += 0.0005f;
<span style="white-space:pre">	</span>setlinecolor( HSLtoRGB(h, s, l) );//画线
line(0, y, 1149, y);
}



其中
HSLtoRGB(h, s, l)
就是用来画线色彩变化的,循环的画线,同时不断调整颜色参数:色相、饱和度、亮度。所画的每根线都不一样,一起显示出来的效果自然就是渐变啦~

在这里总结一下调色的三种方法:

方法一:用预定义颜色常量

常量颜色常量颜色
BLACK0DARKGRAY0x555555深灰
BLUE0xAA0000LIGHTBLUE0xFF5555亮蓝
GREEN0x00AA00绿LIGHTGREEN0x55FF55亮绿
CYAN0xAAAA00LIGHTCYAN0xFFFF55亮青
RED0x0000AALIGHTRED0x5555FF亮红
MAGENTA0xAA00AALIGHTMAGENTA0xFF55FF亮紫
BROWN0x0055AAYELLOW0x55FFFF
LIGHTGRAY0xAAAAAA浅灰WHITE0xFFFFFF
需注意的是:这里用的颜色名都是大写,与HTML里用的略有不同,HTML里用的是小写的。如果你在VC++6.0用小写调颜色的话会报错“ undeclared identifier”,为什么呢?因为这些颜色名都是用宏定义好的,编译器是大小写敏感的,当然会报错咯~

方法二: 用 16 进制的颜色表示,形式为:0xbbggrr (bb=蓝,gg=绿,rr=红),具体参数如上图所示。

方法三:用 RGB 宏合成颜色。这里颜色范围是0~255,比如你可以写setcolor(255, 0, 0);这就是设置成纯红的啦~

方法四:用 HSLtoRGB、HSVtoRGB 转换其他色彩模型到 RGB 颜色。

我这里用的就是HSLtoRGB。

该函数用于转换 HSL 颜色为 RGB 颜色。
COLORREF HSLtoRGB(
float H,
float S,
float L
);

参数:
H
原 HSL 颜色模型的 Hue(色相) 分量,0 <= H < 360。
S
原 HSL 颜色模型的 Saturation(饱和度) 分量,0 <= S <= 1。
L
原 HSL 颜色模型的 Lightness(亮度) 分量,0 <= L <= 1。
返回值:
对应的 RGB 颜色。
说明:
HSL 又称 HLS。
H 是英文 Hue 的首字母,表示色相,即组成可见光谱的单色。红色在 0 度,绿色在 120 度,蓝色在 240 度,以此方向过渡。
S 是英文 Saturation 的首字母,表示饱和度,等于 0 时为灰色。在最大饱和度 1 时,具有最纯的色光。
L 是英文 Lightness 的首字母,表示亮度,等于 0 时为黑色,等于 0.5 时是色彩最鲜明的状态,等于 1 时为白色。
然后再说说成功复原时界面吧~



看到那句"Congratulations!You win~(碰我重新开始哦^_^)"了吗?在成功复原的时候句子就会悠悠的飘~出来,遇到边框还会反弹哦~当鼠标光标触碰字体时就可以重新开始操作。

当时为了实现这个功能,到也调试了挺久的(我是菜鸟,很菜的菜鸟......),先看代码吧:

int word(char word[])                           //运动的字      /*如果用string word(char word[])会有error*/
{
void picture();
//定义字符串
//	string s(word);      //使用TCHAR s[]=_T(word);会出现 error C2440: 'initializing' : cannot convert from 'char []' to 'char []'

FlushMouseMsgBuffer();   //清除鼠标消息缓冲区必须有,不然多次胜利后可能无法接收到"光标触字"指令!

//定义字符串初始位置
int x = 10;
int y = 10;

//获取字符串高度、宽度
int w = textwidth(word);
int h = textheight(word);

//判断移动方向
bool isLeft = false;
bool isUp = false;

BeginBatchDraw();   //开始批量绘图

bool flag = true;
//绘制移动文字
while(flag)
{
FlushBatchDraw();
picture();         //绘制背景

//实现用鼠标达到退出动画的效果
while(MouseHit())
{
MOUSEMSG m = GetMouseMsg();
if((m.x>x)&(m.x<(x+w))&(m.y>y)&(m.y<(y+h)))
{
BeginBatchDraw();   //开始批量绘图(必须有,不然图像显示会出问题)
picture();                 //picture()函数必须放word()函数内调用,防止光标触碰后字体无法及时清除的问题
FlushBatchDraw();  //批量绘图显示picture
EndBatchDraw();   //结束批量绘图
flag = false;
return 0;
}
}

if( x - 10 <= 0 ) isLeft = false;
if( x + w + 10 >= 1150 ) isLeft = true;
if( y - 10 <= 0 ) isUp = false;
if( y + h + 10 >= 500) isUp = true;

if( isLeft )
x -= 10;
else
x += 10;
if( isUp )
y -= 10;
else
y += 10;

outtextxy(x, y, word);

Sleep(100);
}

//退出
EndBatchDraw();
}
其中char和string貌似有点不同

int word(char word[])


如果用string word(char word[])会有error,本来定义所飘动的字符串的时候想用string s(word);用来接收传入的字符串,然而会报错......char和string貌似有点不同?

使用TCHAR s[]=_T(word);会出现 error C2440: 'initializing' : cannot convert from 'char []' to 'char []',不知道是什么原因?

值得一提的是

FlushMouseMsgBuffer();
这个函数用于清除鼠标缓存区,因为程序是不断地获取鼠标位置来判断是否跳出函数,当时我没使用该函数时,由于之前也可能收到了鼠标消息,所以会导致消息滞留,鼠标光标触碰字体的时候的消息很可能没有被及时接受处理,导致消息处理延时失灵。我一开始还以为电脑不灵敏呢,现在看来,一条感悟:自己写的代码运行后没有达到想要的结果时,99%是自己的原因,电脑出错的概率很小!

然后一定要说说批量绘图函数!之前我写一些小的图像觉得电脑运行速度挺快的呀,批量绘图用不用好像没区别吧,然而,这次写转魔方的变幻时,却出现了频闪,这是有原因的,浮动字符串浮动所用的方法是不断调用绘制整个界面的函数用以覆盖之前的界面,从而达到“清除”效果,就好像是图形的绘制是只能加不能减的感觉吧,要用“覆盖”来模拟“擦除”。之前说到彩虹渐变的背景是通过循环改变颜色参数并重复画线直到覆盖整个窗口来实现的,而且我所用的又是整体重绘,所以工程量自然就比简单的小图形大得多啦!所以批量绘图就至关重要啦~

所以说:

<span style="white-space:pre">	</span>BeginBatchDraw();   //开始批量绘图(必须有,不然图像显示会出问题)
picture();                 //背景及魔方的显示
EndBatchDraw();   //结束批量绘图
这段代码很关键呀!

然后以下是字符串撞击屏幕反弹的效果:

if( x - 10 <= 0 ) isLeft = false;
if( x + w + 10 >= 1150 ) isLeft = true;
if( y - 10 <= 0 ) isUp = false;
if( y + h + 10 >= 500) isUp = true;

if( isLeft )
x -= 10;
else
x += 10;
if( isUp )
y -= 10;
else
y += 10;

outtextxy(x, y, word);


其中
if( x + w + 10 >= 1150 ) isLeft = true;
if( y + h + 10 >= 500) isUp = true;


判断距离字符串边缘10像素的地方是否碰到窗口边框。用了变量w和h分别存字符串的宽和高:

//获取字符串高度、宽度
int w = textwidth(word);
int h = textheight(word);


转魔方函数定义如下:

void turn()               //转魔方
{
//void Do(char turn[]);char xx[2];   //不能用此法,不然输入会有延迟效果
void computer();
void coloring();
char turn[100] = "";
bool ifwin();
int word(char word[]);

system("color BD");  //设置控制台前景色和背景色

for(int i = 0; i<100; i++)
{
turn[i] = '1';
}
i = 0;

turn[i] = getch();   //不能用getchar();不然每次都要输入回车
cout<<endl<<"你的步骤如下:"<<endl;
cout<<turn[i]<<" ";
while(turn[i]!='0')
{
switch(turn[i])
{
case'R':Right();break;
case'r':right();break;
case'U':Up();break;
case'u':up();break;
case'L':Left();break;
case'l':left();break;
case'D':Down();break;
case'd':down();break;
case'F':Front();break;
case'f':front();break;
case'B':Back();break;
case'b':back();break;
case'M':M();break;
case'm':m();break;
case'X':X();break;
case'x':x();break;
case'Y':Y();break;
case'y':y();break;
case'Z':Z();break;
case'z':z();break;
case'+':computer();break;  //电脑控制(层先法)
//		case'*':break;    //电脑控制(逆序法)
case'0':exit(1);
default:break;             //不能是continue;不然输入其他键,图形无变化,但也会显示win
}

if(ifwin())        //判断是否复原
{
word("Congratulations!You win~(碰我重新开始哦^_^)");
for(i = 0; i<100; i++)    //重新开始,再次初始化
{
turn[i] = '1';
}
i = 0;
cout<<endl<<"你的步骤如下:"<<endl;
//		turn[i] = getch();   //不能在这儿输入,不然会漏一个
//		cout<<turn[i]<<" ";
}

if(turn[i]=='*'||turn[i+1]!='1')   //按下‘*’后用逆序步骤的方法复原
{
if(turn[i]=='*')
{
cout<<endl<<"电脑逆序复原步骤如下:"<<endl;
}
i--;
if(65<=turn[i]&&turn[i]<=90)
{
turn[i] = turn[i] + 32;
}
else if(97<=turn[i]&&turn[i]<=122)
{
turn[i] = turn[i] - 32;
}
cout<<turn[i]<<" ";
Sleep(1000);
}
else         //没按‘*’或‘+’则继续输入,手工旋转魔方
{
i++;
turn[i] = getch();
cout<<turn[i]<<" ";
}
}
}


对了,还要说一下背景音乐。

首先要在程序顶部写一下

#pragma comment(lib,"Winmm.lib")
然后
mciSendString("open \".\\资源\\background.mp3\" alias mymusic", NULL, 0, NULL);  // 打开背景音乐
mciSendString("play mymusic repeat", NULL, 0, NULL);  // 播放音乐


其中“repeat”用于循环播放。

播放完音效后,记得停止并关闭音乐哦~

// 停止播放并关闭音乐
mciSendString("stop mymusic", NULL, 0, NULL);
mciSendString("close mymusic", NULL, 0, NULL);
因为可以灵活控制音乐的开始、结束、循环等功能,所以用在游戏音效里还是不错的哦~

总之,这次写这个程序还是挺有收获的啦~


                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: