[转载]数据库中char, varchar, nvarchar的差异 – hyddd – 博客园.
[转载]数据库中char, varchar, nvarchar的差异 – hyddd – 博客园.
[转载]那些年,我收集的一些.net函数。分享给大家,项目中有些是经常用到的。 – 青牛客 – 博客园.
分享几个常用的函数,希望对大家有帮助。
一:由于类似在帖子,新闻列表的时候往往有个帖子。这时候就得过滤html标签了。过滤html标签的网上百度下有蛮多的。分享下当摘要中有图片的时候的过滤图片的函数:
public string removeIme(string s)
{
string str = s;
str.Replace("IMG", "img");
string reg = "<img\\s+[^(src>)]*src\\s*=\\s*[\"']{0,1}(?<SRC>[^(\\s\"'>)]*)[\"']{0,1}\\s{0,1}[^>]*>";
while (Regex.IsMatch(str, reg))
{
str = str.Replace(Regex.Match(str, reg).Value, "图片..");
}
二:SQL注入攻击是web网站的安全难题。能否做到防注入也成为了重要课程。除了在SQL语句,存储过程中做功夫外,我分享下对前台用户输入检测的一个函数。
public static string InputText(string text, int maxLength)
{
text = text.Trim();
if (string.IsNullOrEmpty(text))
return string.Empty;
if (text.Length > maxLength)
text = text.Substring(0, maxLength);
text = Regex.Replace(text, "[\\s]{2,}", " "); //two or more spaces
text = Regex.Replace(text, "(<[b|B][r|R]/*>)+|(<[p|P](.|\\n)*?>)", "\n"); //<br>
text = Regex.Replace(text, "(\\s*&[n|N][b|B][s|S][p|P];\\s*)+", " "); //
text = Regex.Replace(text, "<(.|\\n)*?>", string.Empty); //any other tags
text = text.Replace("'", "''");
return text;
}
三:ubb是当前流行,较为完全的代码。也是防范sql注入和跨站脚本的手段之一。分享下将ubb转化成html代码的函数。
public static string decode(string argString)
{
string tString = argString;
if (tString != "")
{
Regex tRegex;
bool tState = true;
tString = tString.Replace("&", "&");
tString = tString.Replace(">", ">");
tString = tString.Replace("<", "<");
tString = tString.Replace("\"", """);
tString = Regex.Replace(tString, @"\[br\]", "<br />", RegexOptions.IgnoreCase);
string[,] tRegexAry = {
{@"\[p\]([^\[]*?)\[\/p\]", "$1<br />"},
{@"\[b\]([^\[]*?)\[\/b\]", "<b>$1</b>"},
{@"\[i\]([^\[]*?)\[\/i\]", "<i>$1</i>"},
{@"\[u\]([^\[]*?)\[\/u\]", "<u>$1</u>"},
{@"\[ol\]([^\[]*?)\[\/ol\]", "<ol>$1</ol>"},
{@"\[ul\]([^\[]*?)\[\/ul\]", "<ul>$1</ul>"},
{@"\[li\]([^\[]*?)\[\/li\]", "<li>$1</li>"},
{@"\[code\]([^\[]*?)\[\/code\]", "<div class=\"ubb_code\">$1</div>"},
{@"\[quote\]([^\[]*?)\[\/quote\]", "<div class=\"ubb_quote\">$1</div>"},
{@"\[color=([^\]]*)\]([^\[]*?)\[\/color\]", "<font style=\"color: $1\">$2</font>"},
{@"\[hilitecolor=([^\]]*)\]([^\[]*?)\[\/hilitecolor\]", "<font style=\"background-color: $1\">$2</font>"},
{@"\[align=([^\]]*)\]([^\[]*?)\[\/align\]", "<div style=\"text-align: $1\">$2</div>"},
{@"\[url=([^\]]*)\]([^\[]*?)\[\/url\]", "<a href=\"$1\">$2</a>"},
{@"\[img\]([^\[]*?)\[\/img\]", "<img src=\"$1\" />"}
};
while (tState)
{
tState = false;
for (int ti = 0; ti < tRegexAry.GetLength(0); ti++)
{
tRegex = new Regex(tRegexAry[ti, 0], RegexOptions.IgnoreCase);
if (tRegex.Match(tString).Success)
{
tState = true;
tString = Regex.Replace(tString, tRegexAry[ti, 0], tRegexAry[ti, 1], RegexOptions.IgnoreCase);
}
}
}
}
return tString;
}
[转载]MVC 4.0 抢先看 – tomin – 博客园.
最近,随着Window 8 的发布,VS11也诞生了,VS11 里面除了添加了Metro风格的开发项目外,在web方面也有所改进,添加了MVC 4.0。俺也偷偷的瞄了2眼,还是有所改变的。例如,相对MVC3.0里面加入了Mobile Application, Web API, Single Page Application 等一些项目模板。如下图:

对于ASP.NET MVC 4.0, 没有说一定要在VS11 里面安装,在VS2010 里面也是可以用的,下面有一些引用,不过都是英文的,相信大家能看的懂 J
ASP.NET MVC 4.0 安装路径:http://www.asp.net/mvc/mvc4
ASP.NET MVC 4.0 新的特性:http://weblogs.asp.net/jgalloway/archive/2012/02/16/asp-net-4-beta-released.aspx
ASP.NET MVC 4 Web API特性:http://www.asp.net/web-api
ASP.NET MVC 4 Mobile 特性:http://www.asp.net/mvc/tutorials/mvc-4/aspnet-mvc-4-mobile-features
ASP.NET MVC Single Page Application: http://www.asp.net/single-page-application
下面我们先看看 Intranet Application, 和Mobile Application 的一些界面。其他的有时间,慢慢和大家分享。
一.Intranet Application:
现在,我们新建一个Intranet Application 项目来看看:
在这里有个问题,就是,当我们新建完成后,你直接运行,程序是Run步起来的。一看就知道应该是权限问题,还好,里面有一个Readme.txt 文件,大家可以看看,只要进行以下设置就行:
IIS Express
1. Right click on the project in Visual Studio and select Use IIS Express.
2. Click on your project in the Solution Explorer to select the project.
3. If the Properties pane is not open, open it (F4).
4. In the Properties pane for your project:
a) Set “Anonymous Authentication” to “Disabled”.
b) Set “Windows Authentication” to “Enabled”.
设置好以后,就可以啦,让我们来看看Home界面:

调正以后,在页面的左上角就显示出了域名和用户了。界面的风格看的也相当的爽。相对MVC3.0 增加了Contact 页面。看下图:

二. Mobile Application
先上界面:

当下载 安装Windows Phone Emulator (RC) 之后,就可以直接去开发phone的Web程序了。还是非常不错的。
后面我会和大家分享更多,希望大家会喜欢。
[转载]InfoQ: ASP.NET MVC 4 浮出水面.
最近,ASP.NET MVC 4的第一个beta版发布了,并且还含有“可以上线”的许可。这意味着即使最终正式版本还没有完成,但微软相信ASP.NET MVC 4目前已经可以在生产环境中使用了。这次的发布包含了对Razon视图引擎的改进、对异步调用的支持,以及WebSockets等功能。
Razor视图引擎
作为ASP.NET MVC的默认视图引擎,Razor中增加了一些新的特性,以减少视图模版的代码量。以~/开头的HTML属性会被自动解析为应用程序的根路径,而不再需要 使用Url.Content。另一个提升效率的特性是可空属性(conditional attribute),例如当class=”@myClass”中的myClass为null时,这个属性将不会被渲染到页面上。
注意:这些新增的特性也会出现在ASP.NET Web Pages 2中。
CSS与JavaScript管理
MVC 4鼓励开发人员合并和压缩(CSS与JavaScript)以缩短传输时间。开发人员不用详细列出每一个客户端需要的文件,只需要使用ResolveBundleUrl函数就可以将某个文件夹中的所有文件包括在内。 Jon Galloway指出,这对于更新某些程序库—如JQuery—特别有用。
移动开发
JQuery Mobile将会被包含在标准移动模版中。这些模版被用于创建一组视图,而这些视图则被Display Modes(译者注:根据用户所使用的设备,如桌面系统或移动设备,选择不同的视图文件进行渲染)子系统所使用。除了默认模式和移动模式,可发人员还可以根据需要添加其他模式。一般系统会根据浏览器发送的user agent来决定使用那种模式,但理论上你可以获得更大的灵活性。
Controller中的异步任务处理
在MVC 4和C# 5中使用异步模型非常简单,难以想象还会有人继续用阻塞的方式编写Controller中的方法。你只需要使用“async Task<ActionResult>”代替ActionResult作为返回值就可以了。当然底层的代码也需要支持异步调用,但基本上只需要做机械的代码转换,任何实习生或初级开发人员都可以处理。
异步Controller还支持cancellation token,Cancellation Token的实例由框架创建,开发人员只需在调用异步方法时,将之作为参数传递即可。这样整个调用栈都可以被取消,而不是简单的终止线程。
查看英文原文:ASP.NET MVC 4 is Live
[转载]如何开发高性能低成本的网站之技术选择 – Daniel Chow – 博客园.
每个企业都是慢慢发展起来的,在起步阶段成本是一个不得不考虑的重大问题 。直接入正题:
前台框架: ASP.NET MVC + JQuery + Json + Flash , ASP.NET MVC 高性能速度快,JQuery 简洁成熟的Js基础框架 , Json 数据格式体积小 ,传输快。Flash 用于开发复杂的页面交互应用。
缓存方案:
Memcached , 基于Key-Value的传统Cache储存方式 , 高性能 , 而且它内置LRU(Least Recently Used)机制自动维护缓存数据,从而 提高缓存的性能和负载能力。
MongoDb , 数据库级别的缓存解决方案 , 适合海量的数据缓存 , 支持查询
权限模型:
基于ASP.NET MVC 的RBAC , 控制对象粒度到Action , 控制操作粒度 是否能访问。权限基于Cookie/缓存记录认证信息 , 在用户登录时就计算出该用户的所有权限并缓存。
(优点:直接通过AOP做横切面控制,不需要设置权限点 ;缺点:无法控制到同一个Action有增、删、改、查等更细的操作粒度,不同的操作需要制作不同的Action , 表面上要多几个Action , 其实这样做职责更加分离,更加符合OO的观点)
多语言解决方案:
服务端, 基于资源文件,完美配合ASP.NET MVC 前段框架 ,进行各项数据验证及提示等
客户端, 同样基于资源文件, 对Page页面采用script 导入序列化的资源文件 ,按名词空间引用 ,如Resources.Book.AreYouSure 的Js变量. 对于flash等可以通过Json 传递。
数据通信:
服务端,WCF , WebService
客户端, HttpRequest 数据类型Json
数据访问层:标准接口化,不对数据实现依赖。
Entity Framwork , 适合只使用SQL Server 的解决方案, 开发效率最高
NHibernate , 支持多数据平台 ,开发效率较高 , 性能一般
ADO.NET, 完全靠开发实现,开发效率低 , 性能较高
性能和效率按正常水平评估
解耦办法:
IOC , 依赖注入 ,
AOP , 横切面拦截 ,权限中的推荐做法
负载均衡:
Nginx , Web前端的负载均衡解决方案 , Nginx 开源免费,高性能 .
页面提速:
实时性要求不高的页面可以做静态化 ,页面的部分动态内容可以通过SSI处理 ,然后数据更新就主动生成页面。页面静态化,通过XSLT的CMS生成机制可以对生成的页面内容进行压缩。
静态资源文件拆分出去做独立站点,加上服务端的GZIP/Deflate压缩等操作,最好配上二级域名,已加快客户端HTTP下载.更加方便以后做CDN.
SSO:
如果有多个站点,统一认证可以降低开发维护等成本.
数据库:
MySQL , 成熟,开源.
[转载]安装MVC项目时自动给IIS添加通配符映射 – Popcorn – 博客园.
在IIS6中安装ASP.NET MVC项目时,需要将aspnet_isapi.dll添加到网站虚拟目录的通配符映射当中,很多时候我们需要手动完成。
这几天弄了个ASP.NET MVC3的项目,写完后做了一个安装部署程序,想安装的时候能自动将aspnet_isapi.dll添加到虚拟目录的通配符映射当中,于是写了下面的 VBS脚本,脚本支持将虚拟目录当作参数传递进来,可以自动给虚拟目录添加通配符映射。可以把这个脚本另存为.vbs文件,然后放到安装类当中使用 System.Diagnostics.Process.Start(vbsFile,virtualPath) 方法进行调用。安装目录可以使用/targetDir=[TARGETDIR]取得,然后处理后获得虚拟目录名。
如何制作安装项目及如何传递参数可以参考:http://kb.cnblogs.com/page/73922/
以下是VBS脚本程序:
Option Explicit
Dim virtualPath
Dim msgTitle
msgTitle = "添加脚本映射"
If WScript.Arguments.Length > 0 Then
virtualPath = WScript.Arguments(0)
Else
virtualPath = "Web"
End If
Public Function AppMap()
Dim oVirtDir
set oVirtDir = Nothing
On Error Resume Next
Set oVirtDir = GetObject("IIS://localhost/W3Svc/1/Root/" & virtualPath)
If Err <> 0 Then
MsgBox "未能创建 IIS 管理对象!" & vbCrLf & Err.Description,vbOKOnly&vbExclamation,msgTitle
End If
If Not oVirtDir Is Nothing Then
MapHandlers oVirtDir
Else
MsgBox "添加映射失败!",vbOKOnly&vbExclamation,msgTitle
End If
On Error GoTo 0
End Function
Sub MapHandlers(oVirtDir)
Err.Number = 0
Dim scriptMaps
scriptMaps = oVirtDir.GetEx("ScriptMaps")
If Err <> 0 Then
MsgBox "未能获取当前脚本映射属性。"& vbCrLf & Err.Description,vbOKOnly&vbExclamation,msgTitle
Exit Sub
End If
Dim iMap
Dim sourceMap
Dim newMap
newMap = ""
For iMap = LBound(scriptMaps) To UBound(scriptMaps)
If Left(scriptMaps(iMap), 6) = ".aspx," Then
sourceMap = scriptMaps(iMap)
'MsgBox "Found aspx: " & newMap
End If
If Left(scriptMaps(iMap), 3) = ".*," Then
'MsgBox "已经添加了映射"
Exit Sub
End If
Next
If sourceMap = "" Then
MsgBox "未能找到aspx脚本映射",vbOKOnly&vbExclamation,msgTitle
exit sub
End If
Redim Preserve scriptMaps(UBound(scriptMaps) + 1)
newMap = Replace(sourceMap, ".aspx,", "*,")
scriptMaps(UBound(scriptMaps)) = newMap
'MsgBox scriptMaps(UBound(scriptMaps))
oVirtDir.PutEx 2, "scriptMaps", scriptMaps
If Err <> 0 Then
MsgBox "保存脚本映射失败!" & vbCrLf & Err.Description,vbOKOnly&vbExclamation,msgTitle
End If
oVirtDir.SetInfo
If Err <> 0 Then
MsgBox "更新虚拟目录信息失败!" & vbCrLf & Err.Description,vbOKOnly&vbExclamation,msgTitle
End If
End Sub
Call AppMap
上面代码的缺点是只能面对一个站点的时候,如果安装的时候服务器有多个站点,代码还需要进行改进。
项目中遇到需要限制validatebox输入字符数的校验,查看了validatebox的帮助发现validType只有一个length[0,100]的校验只是校验输入内容允许字符串长度的范围.于是想到扩展validatebox的validType,于是编写如下扩展代码:
//扩展validatebox 增加录入长度校验
$.extend($.fn.validatebox.defaults.rules, {
mustLength: {
validator: function(value, param) {
return value.length == param[0];
},
message: '输入内容长度{0}字符.'
}
});
使用代码:
<input id="Tag_Code" name="Tag_Code" class="easyui-validatebox" validType="mustLength[10]" required="true"/>
[转载]Windows 8 快捷键列表 – O2DS – 博客园.
最近过于有点不务正业,不过好在工作内容趋于稳定,下周北京开发应该会有更多的时间来完成另外那本书的翻译工作,偷空下午看见MSDN中放出了Windows8的快捷键列表,顺便摘过来。BTW:Windows 8 真是个好系统!
|
Win键+空格 |
切换输入语言和键盘布局 |
|
Win键+, |
查看桌面 |
|
Win键+回车 |
启动“解说员”辅助功能 |
|
Win键+PgUp |
在新的Metro界面和多显示器情况下,将全屏窗口移动到左侧显示器中 |
|
Win键+PgDn |
移动全屏窗口到后侧窗口 |
|
Win键+Shift+.
Win键+. |
捕获应用程序添加或删除到或左或右的捕获程序中 |
|
Win键+c |
打开Charms Bar |
|
Win键+i |
打开“设置” |
|
Win键+k |
打开“设备管理” |
|
Win键+h |
打开“共享” |
|
Win键+q |
打开“搜索” |
|
Win键+w |
打开设置搜索 |
|
Win键+f |
打开文件搜索 |
|
Win键+z |
展开AppBar |
[转载]Android开发八:插曲2–做一个安卓连连看 – 天意人间 – 博客园.
好几天没有写博客了,这几天有点忙,在家里干活阻碍我的学习了,嘿嘿
上次学习的是ListView控件,这一次的小插曲是一个连连看,学了好几天了也该实践一下了,这次用的是一个GridView控件,把从ListView上面学到的数据绑定搬到GridView控件上直接就可以用了。
因为我用的是GridView控件做连连看,网上还是没有这样的例子的,大部分是用的Jbutton和二维数组,因为要把数据绑定到GridView上面,所以我用的是ArrayList。
本程序用了三个晚上的时间,白天没有时间啊,简单的点击消除是实现了,复杂的功能没有,而且也发现Bug了,但是现阶段,我也只能做到这样了,算是个粗制版吧。(这些废话可以直接无视…)
程序用了两个界面来完成。第一个界面就是两个按钮,开始游戏和退出游戏,第二个界面就是游戏界面,代码最后会提供下载,不仔细说了
直接贴上代码吧,已经注释上了
package YYj.llk;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Vector;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.GridView;
import android.widget.SimpleAdapter;
public class main extends Activity {
/** Called when the activity is first created. */
GridView gv1;
int temp=0;
int lastClicked;
int numcolum;
ArrayList> aList;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
gv1=(GridView)findViewById(R.id.gridView1);
aList=new ArrayList>();
//生成数据
CreateStones();
//打乱ArrayList的顺序
MixIt(aList);
//绑定数据
DataBind();
gv1.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView arg0, View arg1, int arg2,
long arg3) {
int temp2=(int)(aList.get(arg2).get("whichStone"));
//第一个条件是判断点击的不是空的,第二个确认两次点击的不是同一个
if (temp2!=R.drawable.ull&lastClicked!=arg2) {
if (temp==0) {
temp=temp2;
}else {
//两个点击的是相同的
if (temp2==temp) {
Point thispoint=arg2topoint(arg2);
Point lastpoint=arg2topoint(lastClicked);
//用下面方法判断是否可以删除
if (CheckIsItCanBeDestoryed(thispoint, lastpoint)) {
Clear(arg2);
Clear(lastClicked);
DataBind();
checkIsSuccess();
}
}
temp=0;
}
lastClicked=arg2;
}
}
});
}
//是否已经全部消除
private void checkIsSuccess() {
for (HashMap amap : aList) {
if ((int)amap.get("whichStone")!=R.drawable.ull) {
return;
}
}
new AlertDialog.Builder(main.this).setTitle("胜利了!").setIcon(android.R.drawable.ic_dialog_alert)
.setMessage("你赢了,是不是特别有成就感呢!!!").setPositiveButton("OK", new OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
Intent intent=new Intent();
intent.setClass(main.this, startActivity.class);
startActivity(intent);
main.this.finish();
}
}).setCancelable(false).show();
}
//判断是否可以消除
private boolean CheckIsItCanBeDestoryed(Point p1,Point p2){
/*判断一条线可以连接的情况*/
if (testVertical(new Point(p1), new Point(p2))) {
return true;
}
if (testHorizontal(new Point(p1), new Point(p2))) {
return true;
}
/*判断两条线可以连接的情况*/
Point newPoint1=new Point(p2.x, p1.y);
int tmp1=pointtoarg2(newPoint1);
if ((int)aList.get(tmp1).get("whichStone")==R.drawable.ull) {
if (testVertical(p2, new Point(newPoint1))&&testHorizontal(p1, new Point(newPoint1))) {
return true;
}
}
Point newPoint2=new Point(p1.x, p2.y);
tmp1=pointtoarg2(newPoint2);
if ((int)aList.get(tmp1).get("whichStone")==R.drawable.ull) {
if (testVertical(p1, new Point(newPoint2))&&testHorizontal(p2, new Point(newPoint2))) {
return true;
}
}
/*判断三条线可以连接的情况*/
Vector vector=new Vector();
vector=Scan(new Point(p1), new Point(p2));
if (!vector.isEmpty()) {
for (int i = 0; i < vector.size(); i++) {
Line line=vector.elementAt(i);
//横线
if (line.dirct==0) {
if (testVertical(new Point(p1), new Point(line.a))&&testVertical(new Point(p2), new Point(line.b))) {
return true;
}
}else {
if (testHorizontal(new Point(p1), new Point(line.a))&&testHorizontal(new Point(p2), new Point(line.b))) {
return true;
}
}
}
}
return false;
}
private Vector Scan(Point p1,Point p2) {
Vector v=new Vector();
//查找A左边的线
for (int y = p1.y; y >=0; y--) {
if ((int)aList.get(pointtoarg2(new Point(p1.x, y))).get("whichStone")==R.drawable.ull&&
(int)aList.get(pointtoarg2(new Point(p2.x, y))).get("whichStone")==R.drawable.ull&&
testHorizontal(new Point(p1.x,y), new Point(p2.x,y))) {
v.add(new Line(0, new Point(p1.x,y), new Point(p2.x,y)));
}
}
//查找A右边边的线
for (int y = p1.y; y =0; x--) {
if ((int)aList.get(pointtoarg2(new Point(x,p1.y))).get("whichStone")==R.drawable.ull&&
(int)aList.get(pointtoarg2(new Point(x, p2.y))).get("whichStone")==R.drawable.ull&&
testVertical(new Point(x,p1.y), new Point(x,p2.y))) {
v.add(new Line(1, new Point(x,p1.y), new Point(x,p2.y)));
}
}
//查找A下面的线
for (int x = p1.x; x if ((int)aList.get(pointtoarg2(new Point(x,p1.y))).get("whichStone")==R.drawable.ull&&
(int)aList.get(pointtoarg2(new Point(x, p2.y))).get("whichStone")==R.drawable.ull&&
testVertical(new Point(x,p1.y), new Point(x,p2.y))) {
v.add(new Line(1, new Point(x,p1.y), new Point(x,p2.y)));
}
}
return v;
}
//判断是否可以用竖线链接两个点
private boolean testVertical(Point p1,Point p2) {
//定义一个bool值,表示循环过程中是否碰到不为空的
boolean b=true;
if (p1.x==p2.x) {
//差值,循环时用到
int temp=(p1.y-p2.y)/Math.abs(p1.y-p2.y);
while(p1.y!=p2.y){
p2.y+=temp;
int arg2=pointtoarg2(p2);
//如果对应坐标点不为空
if((int)aList.get(arg2).get("whichStone")!=R.drawable.ull&p1.y!=p2.y){
b=false;
break;
}
}
}else {
b=false;
}
return b;
}
//判断是否可以用横线链接两个点
private boolean testHorizontal(Point p1,Point p2) {
//定义一个bool值,表示循环过程中是否碰到不为空的
boolean b=true;
if (p1.y==p2.y) {
//差值,循环时用到
int temp=(p1.x-p2.x)/Math.abs(p1.x-p2.x);
while(p1.x!=p2.x){
p2.x+=temp;
int arg2=pointtoarg2(p2);
//如果对应坐标点不为空
if((int)aList.get(arg2).get("whichStone")!=R.drawable.ull&p1.x!=p2.x){
b=false;
break;
}
}
}else {
b=false;
}
return b;
}
//把数字转换为坐标点
private Point arg2topoint(int a){
int px=a%6;
int py=a/6;
return new Point(px, py);
}
//把点转换为数字
private int pointtoarg2(Point a){
return a.y*6+a.x;
}
//生成数据,保证每种图片出现六次
private void CreateStones() {
for (int i = 1; i < 7; i++) {
HashMap hMap=new HashMap();
switch (i) {//这里的判断用到了后面定义的类
case Stones.Blue:
hMap.put("whichStone", R.drawable.blue);
break;
case Stones.Gold:
hMap.put("whichStone", R.drawable.gold);
break;
case Stones.Green:
hMap.put("whichStone", R.drawable.green);
break;
case Stones.Orange:
hMap.put("whichStone", R.drawable.orange);
break;
case Stones.Purple:
hMap.put("whichStone", R.drawable.purple);
break;
case Stones.Red:
hMap.put("whichStone", R.drawable.red);
break;
}
aList.add(hMap);
aList.add(hMap);
aList.add(hMap);
aList.add(hMap);
aList.add(hMap);
aList.add(hMap);
}
}
//消去某个,即为替换为空图像R.drawable.ull
private void Clear(int x) {
HashMap hMap=new HashMap();
hMap.put("whichStone", R.drawable.ull);
aList.set(x, hMap);
}
//绑定数据或者alist改变后重新绑定
private void DataBind() {
SimpleAdapter adapter=new SimpleAdapter(main.this, aList, R.layout.star, new String[]{"whichStone"}, new int[]{R.id.imageView1});
gv1.setAdapter(adapter);
}
//打乱alist中的数据的次序,相当于随机生成
private void MixIt(ArrayList> aList) {
for (int i = 0; i < 200; i++) {
int rd=(int)(Math.random()*aList.size());
HashMap tMap=aList.get(rd);
aList.remove(rd);
aList.add(tMap);
}
}
//内部枚举类
class Stones{
public static final int Ull=0;//这个图片是空白的
public static final int Blue=1;
public static final int Gold=2;
public static final int Green=3;
public static final int Orange=4;
public static final int Purple=5;
public static final int Red=6;
}
//存储坐标点的类,这里用自己写的,没有用原生的
class Point{
int x;
int y;
Point(int px,int py){
x=px;
y=py;
}
Point(Point p){
x=p.x;
y=p.y;
}
}
//这个用来判断三条直线链接的时候用到
class Line{
Point a,b;
int dirct;//1表示竖线,0表示横线
public Line(int dirce,Point a,Point b) {
this.a=a;
this.b=b;
this.dirct=dirce;
}
}
}
代码中创建了大量的Point对象,如果不这样的话在调用的方法中会改变Point的值,这个应该很耗费资源…关键是我基本没有Java基础(就学了十来天)
还有代码中的判断三条线相等的情况参考了:http://www.java3z.com/cwbwebhome/article/article2/2167.jsp?id=530,其他的基本原创吧
附上截图,

源代码,llk.zip
安装包,llk.apk
注:这篇文章是在2004.12完成的,当时是为了向《电脑爱好者》投稿,这是原稿,由于此杂志面向的 读者原因,因此文章中有些地方显得过“白”,在此后,稿子经过两次修改,虽然最终得以发表,但已改得基本上没有太多的技术性了,而且两次改稿下来,一共写 了近6万字,累~~~,现在将其略作修改放在主页上,希望对大家有所帮助)
| 提起 JAVA ,相信大家也不会陌生了吧, JAVA 是一门相当优秀的语言。目前 JAVA 领域 J2EE 、 JSP 、 STRUTS 等技术不知有多么的热门,他们的主要用途是用来进行企业开发, J2ME 也由于能够被大量的移动设备所支持,因此,也有不少的程序,特别是游戏是在 J2ME 平台上开发的,反而是 J2SE ,似乎只是被人们用来做一做程序界面的,就连 APPLET 也很少有人使用了(有了 FLASH ,谁还用 APPLET 啊)。用 JAVA 来开发桌面平台的游戏,似乎很少有人这么做,也可能大家一想到做游戏都会想到 C 、 C++ 、汇编等。
前段日子我迷上的 QQ 游戏中的“连连看”,游戏之余,突发奇想,也想自己用做一个试试,经过十来天的奋战,终于完成了。 我选择了 JAVA 来开发这个游戏,之所以选择 JAVA ,是因为: |
很少有人用 JAVA 来开发桌面游戏,是因为 JAVA 在网络方面的强大优势使人们忽略了 JAVA 在桌面平台上的开发,特别是游戏方面,而并不是因为 JAVA 做不到,而我,却希望通过我的尝试来告诉大家:原来 JAVA 也能做出漂亮的桌面游戏的(我可不是在夸我的程序:))
开发的周期并不是很长,可是开发过程中我也遇到不少困难,也有不少收获,我希望将我的开发过程写下来,与大家共同进步:)
在我的开发过程中,你可以发现我是这么做的:
(之一)动手前的准备
看看别人现成的游戏
在盖房子之前,我们都会先打好地基,然后搭起框架,最后再就是一点一点添砖加瓦,做软件也是一样的道理,都是从大体的框加向细节部分设计实现,现在,我们开始吧。
其实不管是做软件也好,做游戏也好,只要是写程序,在动手之前是一定会存在需求和分析的,如果不经过一定的分析就开始动手写程序,那么,这个程序一定会很难写下去的,最后的结果可能会导致放弃。
那么,在我们动手之前,让我们先简单的分析一下吧。由于“连连看”并不是一个我们凭空开发的游戏,并且网上也 已经有很多别人已经开发好的版本,因此,对于我们来说,我们已经拥有了一个很好的原型(比如说 QQ 游戏中的“连连看”),分析起来也应该是轻松得多。由于 QQ 中的“连连看”是网络版,为了开发上的简便,我们先放弃网络功能,做一个简单的单机版就行了。现在,让我们现在来看一看 QQ 中的连连看吧。

“连连看”的游戏规则其实并不复杂,首先,游戏开始的时候,地图上会有由数张不同的图片随机分散在地图上(并 且每张图片会出现偶数次,通常是 4 次),只需要在地图上找出两张相同的图片(点),并且这两个点之前可以用不超过 3 条的直线连接起来就可以消除这两点,如此下去,直到地图上的点全部消除完就算游戏结束,怎么样,规则很简单吧?:)我们的开发就完全按照些规则来吧。
分析游戏规则找出算法
通过上面的分析,我们已经知道了游戏规则,可是,我们怎么样去实现呢?
其实所谓的实现也就是算法,那我们怎么样找出算法呢?别急,让我们来看一看上图,或者自己动手玩一玩别人做好的。
通过对上图的观察,我们发现,可以将游戏中的地图看作是一个二维数组,其中的所有图片(以下称“点”)可以看 作是数组中的一个具体的元素。那么,游戏中相同的图片可以看作是数组中不同位置两个值相同的元素。至于直线,让我们给组数中的每一个元素赋一个特殊的值如 0 ,以表示地图上空白的位置。并且同时规定:当连续的具有该特殊值的点的横向索引或纵向索引相同时,可以认为这是一条直线,比如下图:

当数组中两点的值相同并且两点间只需要不超过 3 根直线能连接起来的时候,就让这两点的值变为 0 ,如果数组中全是 0 值的点,就认为游戏已经结束:)
怎么样,算法够简单了吧:)
用伪代码来描述程序的结构
现在,我们用伪代码来描述一下游戏,假设用户开始了游戏:
| 准备地图
while ( 等待用户选择点 ) { 当前点 = 用户选择的点 if ( 存在上次选择的点 ) { if ( 当前点与上次选择的点可消除 ) { 消除两点; 上次选择的点 = null ; if ( 地图上已没有可消除的点 ) { 游戏结束; } } else { 上次选择的点 = 当前点; } } else { 上次选择的点 = 当前点; } } 游戏结束; |
看看有没有什么问题?如果没有问题,我们进入下一步吧:)
确定程序需要的模块
当伪代码完成后,并且在我们的大脑里转了几圈发现没有问题后,现在就可以开始进行模块的划分工作了。
我们还是再看一看 QQ 中的“连连看”,整个程序只需要通过鼠标操作就可以了,按照 MVC 的结构来进行程序设计,那么我们需要一个 Model ,用来完成整个程序的核心算法;一个 View ,用来显示用户界面,当然还需要一个 Control ,用来处理用户鼠标的操作,这样一来,只需要三个模块就可以完成了。
现在我们再细想一下,这样真的就可以了吗? Model 是一定需要的,这是整个程序的灵魂。然而对于 Control (控制)来说,控制会分为用户游戏中的操作和游戏提供的功能性操作,如果所有的操作包括游戏中的游戏控制、游戏界面上的帮助、设置等都要通过一个 Control 来完成,那么这个 Control 一定会比较大,并且会比较复杂,而过于复杂的模块通常都是比较容易引起错误,或者导致编码困难的,因此,我们就有必要将具有类似功能的操作分开,以减少各 个模块的复杂程度,同时,也可以使模块的功能更单纯(这也是 OO 中所倡导的)。
现在我们将菜单操作和游戏操作分开,分开后的模块如下:
以上是程序的最主要的模块,除此之外,由于开发过程中的需要,对于每个模块,我们可能还需要一些辅助的模块来使程序更加完善,由于这些模块并不会对程序有太大的影响,因此,我们可以在需要的时候再来添加。
(之二)实现游戏的算法
将游戏地图转换为数组来描述
算法总是很枯燥的,没有直接设计界面来得舒服,然而,算法却是整个程序的核心,所以,仅管枯燥,我们还是得耐心地完成这一步。
在进行程序算法的设计时,我们首先要尽可能抛开一些无关紧要的部分,这样可以使算法看起来直接明了,但同时也要考虑弹性,以便将来扩充。
在前面已经说过了,整个游戏的核心算法也就是以二维数组为主体的算法,那么,定义一个二维数组是必不可少的了。
二维数组究竟应该有多大呢? 10X10 是不是小了, 20*20 呢,大了?究竟多大比较合适?为了考虑到程序以后改动的需要,我们还是定义成变量吧,这样以后要改动的时候,只需要改动一下变量的值就行了,因此,我们现 在为程序增加一个类,使之专门用来保存与程序有关的一些数据。
| //Setting.java
public static final int ROW = 8; // 假设地图有 8 行 public static final int COLUMN = 8; // 假设地图有 8 列 |
至于为什么要定义成 public static final ,这个,自己想想就知道了:)还不知道?晕,看看书吧:(
现在,我们将这个类起名为 Map ,同时,我们规定,为了描述地图中空白的区域,我们使用 0 来表示。
| //Map.java
private int[][] map = new int[Setting.ROW][Setting.COLUMN]; |
初始化游戏地图
在地图初始化的时候,我们需要用一些“随机”的数字来填充这张地图,之所有将“随机”用引号括起来,是因为这些数字并不是真正意义上的随机:首先,数组中具有相同值的元素只能出现 4 次(具有 0 值的元素除外),其次,这些数字是被散乱的分布在数组中的。
要使元素出现 4 次,那么数组中所有不重复的元素个数最大为数组的大小 /4 ,为了简单起先,我们使这些元素的值用 1 、 2 、 3 ……进行编号。
要想将这些分配好的元素再分配到二维数组中,我们需要一个一维数组来辅助完成这项工作。
首先,我们按照二维数组的大小来建立一个大小相同的一维数组,并且,我们规定数组中出现的不重复的元素的个数 (元素个数的多少代表了地图的填充率,填充率越高,表示游戏难度越高),同时,我们也要保证数组的长度能被 4 整除(目前是这样,其实不是必需的),因为相同的元素会出现 4 次。因此,我们定义一个变量,用来表示地图上可能出现元素种类的最大个数,同时也定义一个变量,表示目前地图上出现的元素的个数。
| //Map.java
int[] array = new int[Setting.ROW * Setting.COLUMN]; // 辅助的一维数组 int maxElement = 16; //maxElement 的值不能超过 map 总元素 /4 int elements = 16; // 先假设 maxElement 和 elements 相等 |
在,我们将这些元素放置在一维数组中:
| for (int i = 0; i < max; i++) {
array[i * 4] = i + 1; array[i * 4 + 1] = i + 1; array[i * 4 + 2] = i + 1; array[i * 4 + 3] = i + 1; } |
这时,一维数组初始化完成了,可惜数组中的元素是规规矩矩按顺序出现的,如果不打乱就填充到地图中,这游戏似乎也太简单了(因为相邻的点一定可以消除啊),现在,我们得想个办法打乱这个数组。
怎么打乱这个数组呢?好办,我们来看看,假设数组的原始排列是这样的:
[0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15]
从最后一个元素 [15] 起,依次与此元素之前的某一个元素将值互换,完成后再从 [14] 起,与在 [14] 之前的某一个元素将值互换,直到 [1] 与 [0] 的值互换后,如此一来,数组就被完全打乱了,如果还不明白,我们来看一看下图:
[0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15]
在 [15] 之前有 15 个元素,产生一个 15 以内的随机数,比如说 8 ,再将 [15] 和 [8] 的值互换,变成了如下:
[0] [1] [2] [3] [4] [5] [6] [7] [15] [9] [10] [11] [12] [13] [14] [8]
再从 [14] 号元素开始,产生一个 14 以内的随机数,比如说 10 ,互换 [14] 和 [10] 的值:
改变前:
[0] [1] [2] [3] [4] [5] [6] [7] [15] [9] [10] [11] [12] [13] [14] [8]
改变后:
[0] [1] [2] [3] [4] [5] [6] [7] [15] [9] [14] [11] [12] [13] [10] [8]
怎么样,略施小技就搞定了,简单吧?算法如下:
| int[] random(int[] array) {
java.util.Random random = new java.util.Random(); for (int i = array.length; i > 0; i–) { int j = random.nextInt(i); int temp = array[j]; array[j] = array[i – 1]; array[i – 1] = temp; } return array; // 其实也可以不返回,因为数组的操作总是改变引用的 } |
现在,一维数组中的元素已经被打乱了,现在我们只需要按顺序依次填充回二维数组中就行了,这样,二维数组中的值就一定是乱的。
| for (int i = 0; i < ROW; i++) {
for (int j = 0; j < COLUMN; j++) { map[i][j] = array[i * COLUMN + j]; } } |

( 打乱后的数组,感觉如何,虽然难看了点,但很有用 )
对数组中两个元素是否可以消除的判断
地图的初始化已经完成了,现在的问题是,我们怎么样才能知道数组中的两个元素是否可以消除呢?
根据游戏规则,如果两个点之间可以用不超过 3 条直线连接起来,这两点就可以消除,现在我们来分析一下所有可能的情况:
两点之间只需要一条直线连接:
(图略了……)
由上图可以看出,如果两点间只需要一条直线能够连接起来,则 A 、 B 两点的横坐标或纵坐标必定相同,有了这个条件,我们判断 A 、 B 两点是否只需要一条直接连接就简单了许多。
这段代码比较简单,所以就不写出来了,大家可以看看源程序,只不过需要注意的是,我们将横线连接和竖线连接分开来处理,这样做是为了后面工作的简单。
| boolean verticalMatch(Point a, Point b) // 竖线上的判断
boolean horizonMatch(Point a, Point b) // 横线上的判断 |
( 注意:为了简单省事,我们用 java.awt 包中的 Poin(x, y)t 来描述二维数组中元素的坐标,但是有一点要特别小心, x 和 y 与二维数组中元素的下标值 恰好相反 ,如左上图中 A 的下标为 array[1][0] , Point 的描述却是为 Point(0, 1) ,如果不注意这一点,程序会出错的。 )
两点之间需要两条直线连接:

如上图, A 、 B 两点如果需要两条直线连接起来,有可能有两种方式,于是,我们可以巧妙的构建一个 C 点和一个 D 点,并且规定 C 点的横坐标为 A 点的横坐标, C 点的纵坐标为 B 点的纵坐标, D 点的横坐标为 B 点的横坐标, D 点的纵坐标为 A 点的纵坐标(这一点很重要,因为 C 、 D 决定了 AC 、 BC 、 AD 、 BD 的连线方式),如下图:

如果此时 C 点(或 D 点)能同时满足 AC ( AD )、 BC ( BD )只需要一条直线相连,就表示 A 、 B 之前能够使用两条直线连接起来,并且 C 点( D 点)为拐点(以后会用上的)
| //A 、 B 之间有一个拐点
boolean oneCorner(Point a, Point b) { Point c, d; boolean isMatch; c = new Point(a.x, b.y); d = new Point(b.x, a.y); if (map == 0) { //C 点上必须没有障碍 isMatch = horizonMatch(a, c) && verticalMatch (b, c); if (isMatch) { return isMatch; } } if (map[d.x][d.y] == 0) { //D 点上必须没有障碍 isMatch = verticalMatch (a, d) && horizonMatch (b, d); return isMatch; } return false; } |
( 注意:由于 C 点和 D 点的构建方式确定了 AC 、 BD 永远是竖连线、 BC 、 AD 永远是横连线 )
两点之间需要三条直线连接:
这种方式是最复杂的了,我们还是先分析一下出现三条直线的所有可能性吧。

( 图 A)

( 图 B :这种方式比较容易忽略掉 )

以上图说明了两点间三条直线的所有可能性,和二条直线的情况相比,拐点是两个,麻烦了一点,但也不难处理。
下面我们来分析一下该怎么处理二个拐点的情况(三条直线)。由上面的图可以看出, A 、 B 如果要通过三条直线相连,则必须有 C 、 D 两个拐点,如果能确定下 C 、 D ,问题就好解决多了。
怎么样来确定 C 、 D 两点呢?我们以图 A 中的左图为例,在此之前,我们规定 C 点与 A 点在同一竖线上, D 点与 A 点在同一直线上。同时,从图中我们也可以看出, A 、 B 两点间如果只能通过三条直线连接起来,则必定有一条直线处于 A 、 B 的横向夹线纵向夹线中(如画圈的线)。
我们假设相等的线为在 A 、 B 两点的横坐标相等、纵坐标为 0~Setting.ROW 构成的区域上 ( 如图 ) 。
我们先扫描出所有的线,并且我们发现,如果在 A 、 B 构成的区域中存在两个点能构成直线,那么,这条直线就 有可能 是我们需要的直线,我们称此线为符合线,如果符合线的两端( C 、 D 两点)与 A 、 B 两点分别能 AC 、 CD 、 DB 能构成直线的原则,则 AB 间一定可以通过三条直线连接起来。(这个可能我描述得不太清楚,但相信你应该不难明白的)
我们把所有找到的符合线保存起来,并且要记录下符合线是横向上的还是纵向上的,然后通过这些找到的符合线,依 次和 A 、 B 两点进行判断,一旦找到这样的 C 、 D 两点,能满足 AC 、 CD 、 DB 这三条线上都没有障碍,那么, A 、 B 就可以消除了。还是用算法来描述一下吧。
首先我们构建一个保存 C 、 D 点的类 Line ,并且要指明 C 、 D 的方向是横向还是纵向。
| //Line.java
public class Line { public Point a, b; public int direct; //1 表示横线, 0 表示竖线 public Line() { } public Line(int direct, Point a, Point b) { this.direct = direct; this.a = a; this.b = b; } } |
同时,由于在扫描的过程中,会找到多根符合线,因此,我们可以用 Vector 来保存这些找到的符合线(为了提高效率,也可以使用 LinkedList 来保存)。
| Vector vector = new Vector(); // 保存求解后的线 |
扫描两点构成的矩形内有没有完整的空白线段
| Vector scan(Point a, Point b) {
Vector v = new Vector(); // 从 a, c 连线向 b 扫描,扫描竖线 // 扫描 A 点左边的所有线 for (int y = a.y; y >= 0; y–) { if (map[a.x][y] == 0 && map[b.x][y] == 0 && verticalMatch(new Point(a.x, y), new Point(b.x, y))) { // 存在完整路线 v.add(new Line(0, new Point(a.x, y), new Point(b.x, y))); } } // 扫描 A 点右边的所有线 for (int y = a.y; y < COLUMN; y++) { if (map[a.x][y] == 0 && map[b.x][y] == 0 && verticalMatch(new Point(a.x, y), new Point(b.x, y))) { // 存在完整路线 v.add(new Line(0, new Point(a.x, y), new Point(b.x, y))); } } // 从 a, d 连线向 b 扫描,扫描横线 // 扫描 A 点上面的所有线 for (int x = a.x; x >= 0; x–) { if (map[x][a.y] == 0 && map[x][b.y] == 0 && horizonMatch(new Point(x, a.y), new Point(x, b.y))) { v.add(new Line(1, new Point(x, a.y), new Point(x, b.y))); } } // 扫描 A 点下面的所有线 for (int x = a.x; x < ROW; x++) { if (map[x][a.y] == 0 && map[x][b.y] == 0 && horizonMatch(new Point(x, a.y), new Point(x, b.y))) { v.add(new Line(1, new Point(x, a.y), new Point(x, b.y))); } } return v; } |
现在,我们对所有找到的符合线进行判断,看看 AC 、 DB 是否同样也可以消除
| boolean twoCorner(Point a, Point b) {
vector = scan(a, b); if (vector.isEmpty()) { // 没有完整的空白线段,无解 return false; } for (int index = 0; index < vector.size(); index++) { Line line = (Line) vector.elementAt(index); if (line.direct == 1) { // 横线上的扫描段,找到了竖线 if (verticalMatch(a, line.a) && verticalMatch(b, line.b)) { // 找到了解,返回 return true; } } else { // 竖线上的扫描段,找到了横线 if (horizonMatch(a, line.a) && horizonMatch(b, line.b)) { return true; } } } return false; } |
消除该两个元素时,只需要将两个元素的值置为 0 即可。
更多的功能:自动寻找匹配的点
现在,算法基本上是实现了,但是,为了使游戏更丰富,我们还需要实现更多的功能,现在,我们添加一个自动寻找匹配的点的功能。
该功能需要分两步走:
第一步,从左上向右下搜索二维数组中第一个值不为 0 的元素 A ,找到该点后,然后再从该点向后找到一个值与该点值相等的元素 B ,然后对这两个元素进行是否可消除的判断,如果可以消除,则说明该两点匹配,如果不能消除,则继续寻找与 A 点值相等的 B 点,如果找不到 B 点,则寻找下一个 A 点,依次下去,直到找不到这个 A 点,这就表时地图上已经不存在可消除的点了,我们用伪算法描述如下:
| 找到第一个 A 点
while (A 点存在时 ) { while ( 能找到与 A 点值相等的 B 点 ) { if (Match(A, b)) { 返回找到的 AB 点 ; } } 寻找下一个 A 点 ; } 找不到点 ; |
更多的功能:刷新地图
刷新地图的功能其实非常简单,只是需要将二维数组中现有的元素打乱后然后放回这个二维数组中就行了,我们还是只简单的用伪算法描述一下吧:)
| 找到地图中所有的值不为 0 的点并且保存到一维数组中
打乱一维数组 重新分配回二维数组中 |
完成代码并且测试
现在,算法部分的代码大体上算是完成了,我们可以进行一下测试,测试应该很简单,限于篇幅的原因,我就不在这里写出测试用的代码了,但可以说明一下如何进行测试:
我们可以构建一些特殊的地图,然后用 Match(Point a, Point b) 方法来判断我们指定的两点是否可以消除,或者使用自动寻找的功能,找到相同的两点后,消除这两个点,当地图上没有可消除的点时,就刷新地图,直到点全部消除完成。同时,我们还可以在 horzionMatch(Point a, Point b) 、 verticalMatch(Point a, Point b) 等加上输出语句,来看看匹配时程序执行到哪了,换几个不同的点多测试几次,如果没有问题,那就应该没有问题了:)
(之三)将算法与界面结合起来
用布局和按钮来实现算法的界面
上面已经说完了算法,相信大家也迫不及待的想进入界面的设计了吧,好了,多的不说,我们开始吧。
既然我们的算法是基于二维数组的,那么我们也应该在界面使用一个能反映二维数组的控件。这里有两种方式,一种使用表格来实现,第二种是使用布局来实现。
相对而言,用表格来实现二维数组要简单一些, JAVA 提供的布局方式可能是令大家都不习惯的一种界面设计方式,不过,在这里,我还是选用了布局的方式来实现界面,因为,当你设计完界面后,你会发现 JAVA 的布局也是有它的优点的。
JAVA 提供了 BorderLyout 、 GridLayout 、 FlowLayout 等布局,而在这些布局中, GridLayout 布局是最接近于表格方式的一种布局,同时,我们使用 Jbutton 控件来作为数组中的元素。
| //MapUI.java
public class MapUI extends JPanel implements ActionListener { JButton[] dots = new JButton[Setting.ROW * Setting.COLUMN]; public MapUI() { // 设计布局 GridLayout gridLayout = new GridLayout(); gridLayout.setRows(Setting.ROW); gridLayout.setColumns(Setting.COLUMN); gridLayout.setHgap(2); // 设置纵向间距 gridLayout.setVgap(2); // 设置横向间距 this.setLayout(gridLayout); // 放置按钮 for (int row = 0; row < Setting.ROW; row++) { for (int col = 0; col < Setting.COLUMN; col++) { int index = row * Setting.COLUMN + col; dots[index].addActionListener(this); // 添加事件处理 this.add(dots[index]); } } } } |
当然了,上面的代码太简单了,简单得连一些基本的事情也没有做,比如说在按钮上加上文字,并且,要保证每个按钮上的文字与算法中二维数组中相对位置的元素的值相对应。现在我们运行一下看看,怎么样,像不像?

界面出来了,那我们怎么知道用户点击的是哪个按钮呢。其实这也不难的,我们在放置按钮的时候,对每个按钮的 CommandName 属性赋一个值,通过这个值,我们就可以知道该按钮对应二维数组中元素的具体位置了,现在,我们按放置按钮的代码改动一下:
| // 放置按钮
for (int row = 0; row < Setting.ROW; row++) { for (int col = 0; col < Setting.COLUMN; col++) { int index = row * Setting.COLUMN + col; dots[i].setActionCommand(“” + i); } } |
在按钮的事情处理中,再把这个值取出来,并且还原到坐标上,
| public void actionPerformed(ActionEvent e) {
JButton button = (JButton) e.getSource(); int offset = Integer.parseInt(button.getActionCommand()); int row, col; row = Math.round(offset / Setting.COLUMN); col = offset – row * Setting.COLUMN; } |
怎么样,是不是如此简单?已经知道了用户是按的哪个按钮,并且知道了该按钮对应的二维数组中的值,剩下的事情就不用我说了吧:)
现在,我们需要用 JButton 控件来将算法中二维数组中每个元素的值区别出来,最简单的就是用不同的数字来显示,有了这个功能后,我们就可以很方便的实现界面与算法的同步了。
| // 根据数组来绘置画面
private void paint() { for (int row = 0; row < Setting.ROW; row++) { for (int col = 0; col < Setting.COLUMN; col++) { int index = row * Setting.COLUMN + col; if (map.getMap()[row][col] > 0) { dots[index].setIcon(Kyodai.BlocksIcon[map.getMap()[row][col] – 1]); dots[index].setEnabled(true); } else { dots[index].setIcon(null); dots[index].setText(“” + index); } } } } |
设计用户界面
好了,到现在为止,不管怎么说,我们已经大体上实现了与算法有关的界面,虽然还很难看,但总算是有了个界面,也算是对自己的一点安慰吧。
现在,我们要设计用户的界面了,用户的界面也无需让我们多考虑了,既然已经有现成的,我们就抄吧(其实是我天生缺乏美术天份,与其自己做得难看,还不如抄抄别人现成的)。这部分没有太多要说的,我就用图来说明一下吧。

同样,整个界面也是使用了 JAVA 提供的布局方式,这里使用的是 BorderLayout 布局,我们需要注意的就是先算计好每个区域所需要尺寸的大小,并且使用 JPanel 的 setPreferredSize 方法来固定好大小,同时,根据自己的需要,添加命令按钮如开始、退出等。
为用户界面添加功能
用户界面的设计已经出来了,但是相应的功能我们还没有实现,现在,让我们来一起实现这些功能吧。
同样,我们还是参照一下别人游戏中有哪些功能,并且根据自己的需要来进行选择,在这里,我们只需要提供开始游戏、刷新地图、提示、炸弹等功能就好了(其它的暂时可以不管,以后需要的时候可以再添加)。
开始游戏: 当用户开始游戏的时候,我们需要将地图初始化并且显示出游戏界面 。
刷新地图: 此功能我们在算法部分中已经实现了,现在要做的就是使显示部分能够同步进行。
提示功能: 此功能在算法中也已经实现,我们需要做的就是怎么样将找到的这两个并且向别人指出。
炸弹功能: 此功能其实 就是在提示功能的基础上,加上自动消除两个点。
好了,相信这些对我们并非难事,我们还是继续下一步吧。
(之四)添加更多的功能
计分功能
大体上我们的程序已经可以跑了起来,可惜,就这么玩玩也太没有意思了,总得有个计分的吧。虽然我们不知道别人是怎么计分的,可是,程序是我们自己动手写的,我的地盘我做主,看看我是怎么计分的吧(实现可以放在下一步)。
好了,计分规则定下来了,合理不合理先放在一边,不过,为了方便以后修改计分规则,我们还是将这些定义为常量吧。
| //Setting.java
public final static int limitScore = 4; // 每个方块限定的时间 public final static int timeScore = 2; // 时间奖励的分数 public final static int wrongScore = 1; // 选择失败扣分 public final static int freshScore = 8; // 刷新功能扣分 public final static int hintScore = 10; // 提示功能扣分 public final static int bombScore = 12; // 炸弹功能扣分 public final static int correctScore = 10; // 成功消除后加分 |
现在,我们来一一实现计分功能。
首先,我们定义一个变量来保存用户的总分,另外,消除功能、刷新功能、提示功能和炸弹功能由于都有现成的方法,因此,我们只需要在这些方法中添加上计分功能就行了,唯一麻烦一点的就是计算时间分。
为了满足计时的功能,我们需要在游戏开始的时候记录下当前的时间,同时,在游戏完成时也要记录下完成时的时间,两者相减后就是用户游戏的时间了(如何知道游戏在什么时候结束,这个大家想想,不难办到:))。记录时间可以使用 System.currentTimeMillis() 方法,不过需要注意的是,这是以毫秒来计时的,要记得除以 1000 。
高手排行榜
分数是记下来了,自己每次游戏完成都能知道自己的分数,感觉是不错,不过,光和自己比不够劲啊,要是能和别人比就更好了,嗯,继续满足你的“愿望”:)
(注:以下功能需要一个支持动态页面的空间)
最简单的办法,就是我们在互联网上建立一个保存用户分数的数据库,当用户每次完成后,都让用户可以选择将分数 发送到这个数据库中,同时,我们需要做一个后台程序,该程序接收到用户提交来的姓名和分数后,就保存到数据库中,并且,根据当前的分数,告诉用户当前的 Top10 排行榜。后台程序可以使用网页来实现,至于怎么做,这个与程序关系不大,我们只看一看程序这一块怎么做吧。
首先,做法有两种,一种是自己使用 Socket 类来完成 HTTP 的 Get/Post 操作,另一种就是使用 URL 类,前者适用性前(如 J2ME 就不支持 URL 类)后者用起来方便一些,在此,我们就用后者来实现。
游戏完成后,当用户需要提交分数到互联网上时,先弹出一个对话框让用户输入姓名,同时将用户的姓名和分数,还 有自己已经做好的页面“凑”成一个合法的 URL 地址,然后,将这个 URL 地址简单的 new 一下 (new 的同时就已经将这些数据发送到了互联网上了 ) ,比方说我做的排行榜的页面为 http://www.xhai.com/kyodai/top10.asp ,用户的姓名为“ ZhangJian ”,分数为 2000 分,那么,根据这些信息,我拼凑成的网址就是应该是类似于 http://www.xhai.com/kyodai/top10.asp?name=ZhangJian&score=2000 这样样的地址,然后, new URL(“http://www.xhai.com/kyodai/top10.asp?name=ZhangJian&score= 2000”) 就行了:)是不是够简单的?当然,如果有需要,还可以对这些数据进行一下加密,以防用户“作弊”。 (现在已经废弃)
为了在程序中显示排行榜的页面,我们需要程序能够处理页面的功能,显示,如果自己去实现这个功能,虽然并不复杂,但是 JAVA 提供的 JEditorPane 控件却更适合完成这项工作。
我不想用完整源程序来说明,毕竟那太费篇幅,只是截取一段代码来描述一下吧。
| JEditorPane HelpPane = new JEditorPane();
HelpPane.setEditable(false); // 禁止用户编辑 HelpPane.setPage(new URL(“http://www.xhai.com/kyodai/top10.asp”)); // 设置页面 HelpPane.addHyperlinkListener(this); // 添加用户点击链接时的事件处理 public void hyperlinkUpdate(HyperlinkEvent e) { if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { JEditorPane pane = (JEditorPane) e.getSource(); if (e instanceof HTMLFrameHyperlinkEvent) { HTMLFrameHyperlinkEvent evt = (HTMLFrameHyperlinkEvent) e; HTMLDocument doc = (HTMLDocument) pane.getDocument(); doc.processHTMLFrameHyperlinkEvent(evt); } else { try { pane.setPage(e.getURL()); } catch (Throwable t) { t.printStackTrace(); } } } } |
使用配置文件来保存用户信息
假如用户每次要发送成绩到互联网上时都要输入姓名,那有多麻烦啊,为什么我们不将用户的姓名保存起来呢?
其实,不光是用户的姓名需要保存,以后我们添加的其它选项也需要保存起来,因此,我们必须实现一个保存 / 读取用户信息的类,就如果 Windows 的注册表或者 ini 文件一样。在这里,我不想多费口舌,只是告诉一下大家怎么做就行了。
JAVA 提供了一个 java.util.Properties() 类,这个类就是用来保存 / 读取配置文件的,它的 setProperty() 和 getProperty() 方法就是分别用来保存 / 读取配置文件信息的,就如同使用 ini 文件一样,具体的用法查一查 API 就清楚了。
(之五)完善用户界面
让界面更动起来
整个程序的界面总算是出来了,可惜不太漂亮,这种界面,别说别人,就连自己也不愿意多看几眼,因此,做一些适当的美化工作还是非常有必要的。
想要让界面变得漂亮,最好的办法就是大量使用帖图,可惜,图片太多不仅会影响到程序的执行效率,同时,由于美工不是我们的长项,因此,我们还是走走捷径算了。
首先,我们将各个用户控件设置好背景色,这是最简单的方法了,只要颜色搭配得当,也是最有效的办法了。
其次,为了使界面看上去不那么单薄,因此,我们可以想办法使界面更有立体感。好在 JAVA 为我们提供了许多种 Border 控件,通过 Border 控件来组合其它控件的使用,将会使界面变得有立体感。
第三,使用图片。以上的方法,只会让控件变得漂亮,但控件仍然有控件的影子。而大多数人一看到控件,第一反应就会想起应用程序,而不是游戏。既然我们做的是游戏,那么,我们就可以自己做一些简单的图片来“掩蔽”控件的本来面目。好在这个游戏按钮不多,做几个也不太难。
经过以上的几步操作,界面变得漂亮多了,不是吗?
改变鼠标光标
很少看见过有人改变程序中光标的样子,是不是 JAVA 做不到?其实 JAVA 已经考虑到了这一点,只不过很少有人想去这么做这已。 createCustomCursor 就是为我们准备的,其具体用法是:
createCustomCursor(Image cursor, Point hotSpot, String name)
cursor 是我们要设置为光标的图片, hotSpot 是图片显示在实际光标位置的位移, name 就是光标的名字拉!
好了,现在我们找一张合适的图片来作为程序的光标吧,看看效果如何?
如果,你还不满意,或者,你要说:我们的光标不能动啊,人家 QQ 上的光标可是会动的呢。
这确实有点麻烦,因为 JAVA 提供的方法只能显示静态的光标,但是,通过一些简单的方法,我们还是可以实现的。
由于 JAVA 的光标只能是静态图片,因此,要显示动态的光标,我们只能是定时更改光标的图片,首先,我们准备好一系列图片,然后,我们需要使用 javax.swing.Timer(int, java.awt.event.ActionListener) 方法来设置一个定时器,当定时器的事件触发后,我们就改变光标显示的图片。在本程序中,由于考虑到效率问题,我们就没有使用动态光标了,不过,如果你有兴趣,可以试试的:)
将时间 / 分数的显示作为动画来显示
为了让程序更有活力,我们可以适当的将游戏中一些显示信息的地方做成小动画,比如说时间和分数。
在动画的处理过程中,我们要保证动画只是起到作为游戏的点缀,而不能影响到游戏的正常进行(比如说不能在动画进行的过程中中断游戏),同时,动画也不能太喧宾夺主,这样也会分散别人在游戏中的注意力的。
为了保证动画过程和游戏过程的平行运行,因此,我们非常有必要将动画分离成一个独立的控件,并且要保证动画有自己单独的线程来运行。好了,现在我们先来看看我们怎么把时间作为动画分离出来的吧。
| //ClockAnimate.java
public class ClockAnimate extends JPanel // 将时间的显示作为 Panel 控件 implements Runnable { // 使用线程保证动画的独立性 public ClockAnimate() { this.setPreferredSize(new Dimension(156, 48)); // 设置好控件的大小 } 现在,我们就做一个的数字变化的效果,这种效果最简单的方式就是让数字每隔一段时间就变化一次。 public void start() { startTime = System.currentTimeMillis(); // 当线程起动时,记录下当前的时间 thread = new Thread(this); thread.start(); // 线程开始运行 } public void run() { // 线程运行的主过程 Thread currentThread = Thread.currentThread(); while (thread == currentThread) { long time = System.currentTimeMillis(); usedTime = time – startTime; try { repaint(); // 重画数字 thread.sleep( 100l); // 延时 100 毫秒,即 0.1 秒 } catch (InterruptedException ex) { } } } public void paint(Graphics g) { // 重画时间 g.drawString(“Time:” + usedTime, 16, 40); } |
怎么样,时间的显示是不是可以动了?为了使文字在使用大字体的情况下显示得更漂亮一些,我们可以适当的使用抗锯齿效果, JAVA 提供了现成的方法,很简单的,现在我们将 paint(Graphics g) 改动一下:
| public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D) g; Dimension d = getSize(); g2.setBackground(new Color(111, 146, 212)); g2.clearRect(0, 0, d.width, d.height); // 使用背景色清除当前的显示区域 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 打开抗锯齿效果 g2.setColor(new Color(212, 255, 200)); g2.setFont(new Font(“serif”, Font.PLAIN, 28)); g2.drawString(“Time:” + getTime(), 16, 40); } |
Graphics2D 是 JAVA 提供的增强型图形处理包,可以实现许多以前 Graphics 实现不了的功能。在由于系统记录的时间是以毫秒为单位记录的,因此,在上面我们需要写一个 getTime() 方法来将时间的显示格式化成类似于 123.4 这种形式。
时间的动画完成了,现在我们开始制作分数变化的动画。其实分数动画的基本设计方法与时间动画相同,但是有一点不同的是,时间动画在用户游戏的整个过程中是一直运行的,而分数的动画是要根据用户当前得分的情况进行变化的,也就是说,分数的动画是被用户干预的。
现在,我们将动画运行的主过程改动一下。为了简单起见,我们只考虑分数从低向高的变化,不考虑分数从高向低的变化。
| public void run() {
Thread currentThread = Thread.currentThread(); while (thread == currentThread && lastScore < currentScore) { try { lastScore++; repaint(); thread.sleep( 50l); } catch (InterruptedException ex) { } } } public void setScore(int l, int c) { // 根据用户得分前后的数值进行动画处理 this.lastScore = l; this.currentScore = c; start(); } |
当每次用户的分数发生变化时,我们可以使用 setScore(int l, int c) 方法同步分数显示的动画效果。
实现消除图片时的动画效果
在完成了上面两个动画,现在我们来设计消除图片时的动画,动画的效果是当我们选中两个可消除的图片后,找到两个图片之间的连接直线,并且从第一个选中的图片向第二个选中的图片之间做点的拖尾效果的动画(如果不是很明白,看看程序运行的效果就知道了)。
这个动画比前两个动画处理起来要麻烦得多,因为,它牵扯到的部分比较多:首先,需要记录下两点之间的直线以及 这些直线出现的先后次序,其次,还需要知道每条直线的方向,是横向还是纵向,第三,还需要在这些直线上依次进行动画效果的处理。这个动画效果不仅牵扯到了 界面,还牵扯到了算法。现在,我们还是一起看看怎么实现吧。
首先,在算法中,当每次消除两个点的时候,我们就需要记录下这两个点之间的连线情况,是横着的还是竖着的,是 1 条直线还是 2 条或者 3 条。因此,对于 Map.java 中的 verticalMatch(Point a, Point b) 方法就必须改动一下
| private boolean verticalMatch(Point a, Point b, boolean recorder) {
………… if (!test && recorder) { // 如果当前不是测试并且要求记录路径 animate = new AnimateDelete(0, a, b); } return true; } |
recorder 说明了当前是否要求记录下路径,比如说在使用提示功能的时候,虽然我们是会调用该方法来进行试控两点是否可以消除,但实际上,这两个点并不是真的需要消除,所以,在这种情况下,就不应该记录下路径。
AnimateDelete 是我们新创建的一个类,其作用就是处理消除时的动画效果的,该类有几个构造函数,依次如下:
| public class AnimateDelete
implements Runnable { // 获得界面上的 JButton 控件 public AnimateDelete(JButton[] dots) { this.dots = dots; } // 一条直线的情况 //direct 方向, 1 表示 a, b 在同一直线上, 0 表示 a, b 在同一竖线上 public AnimateDelete(int direct, Point a, Point b) { …… } // 两条直线的情况 //direct 方向, 1 表示 a, b 在同一直线上, b, c 在同一竖线上; //0 表示 a, b 在同一竖线上, b, c 在同一直线上 public AnimateDelete(int direct, Point a, Point b, Point c) { …… } // 三条直线的情况 //direct 1 表示 a, b 为横线, b, c 为竖线 , c, d 为横线 //0 表示 a, b 为竖线, b, c 为横线, c, d 为竖线 public AnimateDelete(int direct, Point a, Point b, Point c, Point d) { …… } |
上面的 public AnimateDelete(JButton[] dots) 构造函数看起来似乎没用,实际上,这个是非常有用的,我后面会提到的。
好了,现在可以在 Map 算法中的 horizonMatch 、 verticalMatch 、 oneCorner 、 twoCorner 等方法中添加消除动画的构造函数了。
在 AnimateDelete 的几个构造函数中,还要记得将每种方式中涉及到的直线的路径记录下来,最后,将路径上的这些元素依次保存在一个一维数组中,同时,我们也需要记录下路径的长度,以便动画时的操作。现在我们来看看动画部分如何处理。
| public void run() {
if (count < 2) { //count 是路径的长度,当 count<2 的时候,不进行动画 return; } Thread currentThread = Thread.currentThread(); boolean animate = true; while (thread == currentThread && animate) { // 先用图片来填充经过的路径 for (int i = 1; i < count – 1; i++) { dots[array[i]].setEnabled(true); dots[array[i]].setIcon(Kyodai.GuideIcon); try { thread.sleep( 20l); } catch (InterruptedException ex) { } } // 然后恢复经过的路径 for (int i = 1; i < count – 1; i++) { dots[array[i]].setIcon(null); dots[array[i]].setEnabled(false); try { thread.sleep( 20l); } catch (InterruptedException ex) { } } // 消除两点 dots[array[0]].setIcon(null); dots[array[0]].setEnabled(false); dots[array[count – 1]].setIcon(null); dots[array[count – 1]].setEnabled(false); animate = false; } stop(); // 停止动画 } |
由于消除动画可以几个消除动画同时进行,因此,对于此效果,我们就要在每次使用该效果时实例化一个该对象了。
为程序添加声音
现在,我们来为我们的游戏添加声音。虽然 JAVA 自从出道之日起就能够处理声音,但是那只限于在 APPLET 中进行处理,即便是这样,声音的格式也只能是少见的 AU 格式。幸好, SUN 意识到了这个问题,在 JDK1.3 之后, JAVA 就提供了专门的声音处理包来满足声音处理的需求。 javax.sound.midi 和 javax.sound.sampled 就是分别是用来处理 MIDI 和波形文件的,虽然 JAVA 提供的这两个包还不支持如 MP3 、 RM 等这类格式的文件,但是对于我们的这个游戏来说,能处理 MIDI 和 WAV 文件也已经够用了。
MIDI 格式的文件其优点在于文件小,但缺点是只能保存乐曲而无法包含声音信息, WAV 格式虽然能包含声音信息,可惜文件太大。因此,我们选用 MIDI 来作为游戏的背景音乐,而 WAV 来作为音效。
我们先来看看如何处理 MIDI 格式的文件吧。
| // 读取 midi 文件
public void loadMidi(String filename) throws IOException, InvalidMidiDataException { URLClassLoader urlLoader = (URLClassLoader)this.getClass().getClassLoader(); URL url = urlLoader.findResource(filename); sequence = MidiSystem.getSequence(url); //sequence 保存着 MIDI 的音序结构 } // 播放 sequence public void play() { if (isPlaying) { // 如果已经在播放,返回 return; } try { sequencer = MidiSystem.getSequencer(); sequencer.open(); sequencer.setSequence(sequence); // 加载 sequence sequencer.addMetaEventListener(this); // 添加事件处理 } catch (InvalidMidiDataException ex) { } catch (MidiUnavailableException e) { } // Start playing thread = new Thread(this); thread.start(); } public void run() { Thread currentThread = Thread.currentThread(); while (currentThread == thread && !isPlaying) { // 当 MIDI 没有播放的时候,播放 MIDI 音乐 sequencer.start(); isPlaying = true; try { thread.sleep( 1000l); } catch (InterruptedException ex) { } } } |
代码很短,但是已经能很好的完成我们需要的功能了,当然,如果你还嫌不满的话, JAVA 也提供了多种方法让你对 MIDI 格式的文件进行音调、频率的改变,由于这方面要牵扯到比较专业的知道,而我也不太了解,因此我就不说了:)
对 WAV 格式文件的操作和 MIDI 的操作基本上很类似,只不过使用的 API 包不同罢了,具体的我就不多说了,大家看看源代码就知道了。
让用户了解游戏规则
并非所有的人都玩过这个游戏,也并非所有的人都了解游戏的规则,因此,做一个帮助系统对刚接触的用户来说,还是非常有必要的。制作帮助系统非常简单,无非就是用一个对话框来显示游戏规则,显示的方式有多种,可以使用 JLabel 控件来显示,也可以使用 JTextArea 控件来显示,当然,在这里,为了使帮助系统更完美,使用 HTML 来制作帮助系统将来是最佳选择,由于在上面已经提到过如何使用 JEditorPane 控件显示 HTML 页面,因此,这个就留给大家自己完成吧。
并且使用自己的偏好来进行游戏
虽然我们为游戏提供了许多功能,可是并非所有的用户都能完全接受这些功能的,因此,我们需要提供一些设置使用户能够使用自己的偏好来进行游戏。
在此,我们提供了用户选择打开 / 关闭背景音乐、打开 / 关闭游戏音效、设置游戏的难度、设置消除动画的速度这 4 项功能,为了使用户能够自己设置,我们不仅需要使用上面提到过的配置文件来保存信息,还需要在程序中提供设置界面,在此,我们再添加一个 SetupDialog 类,这个类并不难实现,大家看看源程序就可以了。
(之六)优化:让程序运行更稳定、更高效
改善游戏的合理性
到目前为止,我们的游戏基本上算是完成了,为了使程序更合理,我们还需要将整个程序从头再理一遍,看看有没有改进的地方。
首先,在变量的使用上,由于很多地方会使用相同的数据,因此,将变量定义为 public static 可以减少不必要的重复定义,如颜色、使用的图片等。
其次,由于 JAVA 的特性,我们可以对程序进行简单的修改(如使类从 JApplet 继承而不是从 JFrame 继承等),将其改变为 Applet 程序,让游戏在网页中也可以运行。同时,也考虑到程序的发布,我们可以将最终结果生成 JAR 包。
第三、为了确保在游戏运行时所有需要的资源都已经加载,因此,我们需要使用 MediaTracker 的 addImage(Image image, int id) 和 waitForID(int id) 方法来保证资源加载。
第四、为了避免每次使用资源时都要从硬盘中读取,因此,我们尽可能对所有的资源都要进行缓冲处理,这是以空间换时间的做法。
第五、由于在最后发布程序的时候,我们需要将编译结果打包,因此,对于程序中的图片、声音等也需要放在包中, 这样,对文件的访问就不能使用 File 类了,必须使用 java.lang.ClassLoader.findResource(java.lang.String) 来访问包中的资源。
节约内存的使用
虽然 JAVA 提供的垃圾回收机制使我们可以不必考虑内存分配 / 回收的问题,然而,考虑到运行游戏的客户端多样性,因此,我们还是有必要减少内存开销的。还记得在前面我们提到过 AnimateDelete 类中的 public AnimateDelete(JButton[] dots) 构造函数吗?这个看似无用的方法其实是非常有用的。
在前面,我们提及过,由于多个消除动画可以同步进行,因此,如果在每次实例化类的时候都将界面上的 JButton 控件复制进来,这是一笔不小的内存开支,因此,我们在 AnimateDelete 类中将接收到的 JButton 控件定义为 static ,这样,所有的实例都可以共享同一个 JButton 控件,表面上看起来定义 static 变量使得这部分内存的开销无法除,但考虑到 AnimateDelete 类调用的频率,因此,这样反而是节约了内存开支的。
同样,如果大家看看我的源程序,会发现我有一些地方都将变量定义为了 static 。
(之七)扩展我们的程序
到此为止,我们的开发算是告一段落了,虽然在开发过程中有不少的苦与乐,但是,我们终于完成了这件作品,对自己也算是一点安慰了。
尽管我的文笔可能不流畅,也可能表达不清楚,但最重要的是你能跟着完成这个游戏。
J2ME 版本
想想,如果这个游戏能跑在手机上,这将是多么愉快的一件事情啊。
手机上的开发和 PC 上开发的区别在于:
1、 由于受限制于设备屏幕的大小,因此,在设计时需要充分考虑好界面的大小
2、 在手机上,由于处理器远远没有 PC 处理器的性能强劲,因此,动画将会被省略掉
3、 各种型号的手机对声音处理的包可能不完全相同,因此,如果强调通用性,声音也会被省略掉
4、 手机上没有 GridLayout 布局,没有 Button 控件,因此,所有的布局必须自己使用 Canvas 来绘制
5、 ……
尽管有着如此多的缺点,能完成手机上的版本依然值得期待,你愿意试试吗?
联机对战版本
想要让你的程序能够支持联机大战吗,看看我这么设计是否合理。
1、 使用 Socket 来进行通讯
2、 使用一个独立的服务器进程来完成各个客户端之间的通讯
3、 定义好每次通讯时传递信息的描述方式
4、 保证客户端的同步
5、 ……