随着手机普及率的提高以及附加功能越来越丰富,被称为“拇指一族”的手机玩家的数量也急剧攀升,形成了一个相当特殊的消费族群。日前,记者在“空中网”的会客室里见到国内最大手机在线游戏“夺宝中华”的头号玩家陈剑(化名)。这位被称做“中国手机游戏第一人”的传奇玩家,给人的第一印象并不太起眼:三十来岁的年纪,略显瘦削的身材,一脸疲惫的神情。
-最适合我的娱乐方式
——“它没有富丽堂皇的画面和音效,但它是一种可以移动的娱乐。”
作为某名牌大学的毕业生,陈先生眼下在深圳拥有一家化工贸易公司。由于生意上的缘故,他使用手机的历史可以追溯到10年前,当时他就已经是中国移动的大客户了。
三年多以前,陈先生首次接触手机游戏,起初只是感觉好奇,并没有怎么上瘾。直到在不久后加入了空中网的“夺宝中华”才真正被它吸引住了。
与那些瞄准运营商提供的丰厚奖品的玩家不同,对于每天为生意奔波的陈先生来说,手机在线游戏只是最适合他的一种娱乐方式,“与PC在线游戏不同,它没有富丽堂皇的画面和音效,但它是一种可以移动的娱乐,不论到哪里,只要能接收到GPRS的信号,就可以玩,很方便。”
-两年手机费花了三万元
——“我只是想让自己的生活充实起来,谈不到什么'丧志'。”
后来,陈先生越玩越入迷,曾经创下了连续18个小时在线的纪录——“那时,一睁眼就是'夺宝'”。在短短的两年中,他光是在手机资费方面就花去了3万多元。
陈先生说,今年的前五个月里,“夺宝中华”在搞全国排名,他几乎每天打十六七个小时,名次自然是突飞猛进。但后来,陈先生有一个多月中在游戏里无所事事,“因为我能完成的任务还没有在游戏中开放呢。”
在“夺宝”的两年里,陈先生为了在游戏中保持自己的领先地位,连续换了五部手机。如今,他每天都带三部手机在身边,每一部随时都可以上线。
大量的金钱投入再加上体力透支,生活和事业必然会受到一定的影响。“在玩得最疯的那一段,如果有订单来,我都是让别人去接,非要我去的,我就今天拖明天,明天拖后天……真是耽误了一些生意。”不过,他并不同意有人批评他“玩物丧志”,“我如今虽然不算非常富有,但也有一定的经济基础了,我只是想让自己的生活充实起来,谈不到什么丧志。至于为此损失的,我会想办法弥补。”
-游戏中交的朋友无话不谈
——在“夺宝中华”排名前十位的玩家基本上都是广东人。
既然是一种在线交流的游戏方式,自然少不了会结交到一些朋友,而这也与游戏用户所在的区域有关。根据“空中网”的统计显示,在所有“夺宝中华”的注册玩家中,大多都来自南部沿海发达的经济城市,尤以广东、福建、浙江的手机用户最多。
陈先生说,他所接触的玩友大多都与他的年龄、经历相仿,虽然事业都小有成就,但精神却十分空虚,“极无聊的那种”。在游戏中,大家找到了许多的乐趣,很快就成为了无话不谈的好友。据他介绍,如今在“夺宝中华”排名前十位的玩家基本上都是广东人,年龄都在30来岁。他们曾在线下进行过交流,但主要还是通过电话联系,基本上没有见过面。
-高级玩家手机号能卖上万元
——“我的号码曾经被开价到8000元,现在估计怎么也上万了。”
随着游戏乐趣逐渐减少,许多玩家都会选择放弃游戏,而手机中那个积攒的丰富经验值和道具装备的虚拟角色,便成了不折不扣的“鸡肋”。另一方面,又总有些低级别的玩家想“不劳而获”,而这些“鸡肋”便自然而然地成为了他们渴求的目标。
有需求,就会有交易。和PC在线游戏一样,手机玩家之间也会为此进行私下的交易。据说在“夺宝中华”中,一个高等级的注册号码可以卖到千元以上。“我的一个朋友就把他的号码卖了3000多元,”陈先生说:“我的号码曾经也被开价到8000元,现在估计怎么也上万了!”
付出的比收获多
由于任务开放的局限和升级越来越困难,陈先生现在的游戏时间已大幅减少,每天大约只有六七个小时。即便这样,他在游戏中也依然是级别最高的王者,向他挑战成功便意味着新的传奇的诞生。为此,他经常能碰到成群结伙前来叫板的玩家。有一次甚至曾同时与10名玩家对阵,多亏自己的帮派兄弟及时赶到,才将对手打得狼狈收场。
在采访过程中,记者发现陈先生的手一直在微微颤抖,而且还时不时地望望摆在桌子上的手机。当他在讲话中挥动右手时,手掌下部清晰地显出一块凹陷的痕迹,他戏称是夺宝“夺”出来的老茧。看来,手机游戏早已和他生活中的方方面面融为一体了。
玩游戏到了较高的等级,手机号就可以卖出少则上千、多则上万的高价。但陈先生表示对此不屑一顾:“我说他们是学别人装大款罢了。其实,就是真出这么多钱,我也绝对不卖的。毕竟是自己的心血结晶,多少个日日夜夜不眠不休,岂是这点儿钱能换来的?”
记者问到玩手机游戏最大的收获是什么陈先生笑了笑:“肯定是付出的更多一些。”他婉转地表示,由于平时工作的压力太大,没有一个稳定的排解渠道,而手机游戏恰恰带给了他一种舒缓情绪的方式。“平时在生活中,你付出和得到的往往不会成正比;而在游戏中,只要你肯付出、肯花精力,就一定能成功。”而这份超越现实的满足感,也许就是他最大的收获。
记得第一次见到FishMan是在清华大学南门旁边的一个餐馆里,当时新浪的好几个版主也都在,Fishman请客,第一印象就是很帅,不大象一个软件公司的CEO,到象是电影明星。他的真名吴锡桑,读出来怪怪的,感觉远不如FishMan这个网名好记。FishMan自己也很喜欢这个网名,有时候还戏称自己为“漏网之鱼”,那次聊天的时候,Fishman谈到自己曾经做过4年的水木清华BBS的技术栏目版主,谈到这些,眼神和语气中透出更多的自信,他说很多朋友都是那时认识的,包括3721的CTO周鸿一,至今,现在水木清华的BBS上还保留着很多他的技术文章。
不过现在FishMan已经成功的将自己的角色转变为了一个CEO,他现在领导的天下网络软件公司正积极的扩展自己网络游戏的地域,而自己也从一个埋头于编程的开发人员转变为了经常穿梭于各地各大公司之间的决策和管理人。Fishman是一个真正从软件开发人员一步步转变为CEO,其中的过程和艰辛也只有他自己最清楚。
初露锋芒
FishMan来自广东的海滨城市汕尾,91年进入暨南大学计算机科学系。刚进大学 参观计算中心的时候,算是第一次见到真正的计算机。此前因为喜欢玩街边的 电子游戏机而报考的电脑专业。比起一进机房就能刷拉刷拉拨弄键盘的同班同学,他觉得简直有些无地自容。然而很快便投入到了对电脑的热情当中,对这些复杂的东西很感兴趣,整天泡在系里的电脑机房,而且慢慢地有了超前意识,开始在图书馆查阅《计算机世界》,《电脑》和《中国计算机用户》电脑报刊等,并把学到的内容进行实践。刚开始是依样画葫芦,后来渐渐地有了自己的思维。倒了1994年,随着Windows编程和多媒体技术的兴起,出现了许多各种题材的多媒体光盘。他开始研究使用原型开发工具技术, 开发一套类似Macromedia Authorware的《南粤多媒体开发平台》,可以用这个工具制作多媒体光盘,软件演示和多媒体课件等等。经过半年的努力,Fishman顺利完成了《南粤多媒体开发平台》的1.0版本。几年的努力终于有了点收获。并为此获得了广东省科委组织专家鉴定的1995年广东省高校杯软件比赛的第一名,并获得计算机世界奖学金。后来他的这个软件还代表代表暨南大学参加1995年12月在武汉大学举行的“挑战杯”全国大学生竞赛,并取得了广东省高校历年来最好成绩。
那个时代,同很多年轻人一样,求伯君,朱崇军作为成功的程序员也是FishMan的偶像,他们都怀着自己创业的激情。单枪匹马闯天下,毕业后,一个香港的富商投资30万,帮助FishMan在1995年底就开办了自己的公司:广州飞鹰电脑公司。而这个公司的也成为了Fishman再一次的转折点,同时也成为一段崎岖路程的开始。
创业的艰辛
当时,飞鹰电脑公司是一家不足10个人的微型IT公司。由于资金的限制,他只能用最少的钱去做最好的事。Fishman已经开始担当的不仅仅是程序员的角色了。他不但要做技术,而且要策划市场推广,销售,代理合作,技术支持。虽然很劳累,但他能够顶得主,但投资方派来的一个在国家单位混日子的50来岁的干部,一个连IT怎么拼写都不知道的总经理确使他领略到了创业的艰辛和无法忍受的束缚。
Fishman很清楚每个版本的软件都有它的生命周期和不足,看着Internet的逐渐普及,97年初他就开始提议开发飞鹰4.0版,增加Internet特性,遭到总经理的否决。有几个公司和单位看上他们的产品,希望能投资飞鹰电脑公司,遭到总经理的否决。
在飞鹰3.0版获得1997年12月第三届中国PC应用软件设计大奖赛优胜软件, 名列工具类软件第一,并因此获得了中国软件行业协会理事的职位的时候,IDG派人前来飞鹰电脑公司洽谈,希望投资进来, 要求提供一份商业计划书,再次遭到总经理的否决!
更为强烈的打击接踵而至,公司为了其他的事情决定暂停飞鹰4.0版的开发。这下使得本来就劳累过度的FishMan无法承受,一怒之下病倒了。并于98年7月离开了飞鹰电脑公司。后来他提到这段痛苦的经历将他带入了更深层次的思考之中,开始静下心来思考中国软件产业的问题。他认为:中国模式的资金与技术的合作,受伤的总是技术人员。很多著名的程序员都吃过这个亏,并且提到了周鸿伟、王志东等等。中国的很多软件企业,仍然摆脱不了原始的作坊模式,原始的管理模式。几个人找台电脑找个地方就是一家软件公司,能真正走向成功的少之又少。中国的软件产品,仍然只是仅有的几个品种。 在国外微软等航空母舰型企业的唑唑进逼和盗版小贩的摇旗呐喊之下,很多软件企业在中国IT史上闪了一下就消失了。那时的Fishman极其痛苦,亲眼身边搞技术的朋友一个个去了美国,不禁叹息:难道只有美国才是软件技术人员的乐土吗?
网络使Fishman开始了第二次创业
网络给了程序员很大的机遇和挑战,很多公司和个人赶上了网络的大潮,便成就了一番事业,而有很多人并没有意识到网络带给程序员的机会,于是现在还是程序员。其实对于程序员,对技术发展方向的敏感性决定了是否能够在转折的时候把握住这一切。程序员是关注技术的,但不等同于只有技术。
网络风暴开始悄无声息的刮了起来,99年1月,FishMan加盟了刚成立的博大国际互联网公司,成为了博大公司的第二位员工。不久之后,在他的带领下,博大推出了邮件列表引擎,调查引擎,域名转向和网络辞海等产品。
偶然的一个机会,Fishman遇到了中文热讯的创始人之一的陈仲文,陈仲文问FishMan为什么要出国,他说:“我需要一个新的起点,不想再做作坊式开发的软件。我需要一个软件公司,有足够的开发人员,规范化的开发模式和国际营运模式的背景。希望把软件公司做大,效益做好”。可以说,陈仲文是一个让Fishman感受到机会就在我身边的人。他建立中文热讯的时候,根本就不看好。几个人弄了不到一万元拼命做没有技术含量的网页,被拷贝和取代的可能性很大,也就是说,成功的概率太小。可是他们成功了,先是被IDG注资,后来合并成梦想热讯集团。
二人的想法非常的一致,安装这个考虑,他们拟定了两份商业计划书。1999年11月,成功的从香港Myrice总公司吸引了投资,创立了仙童数码科技有限公司(CNTOMI.COM)。没有过多久,另外一份创办网络游戏软件的商业计划书也得到了投资,这就是后来成为了国内最大的网络图形MUD游戏开发商天夏科技有限公司。在仙童数码,他的角色是CTO,负责代表公司技术形象、策划公司产品的开发、技术难题攻关、规范开发流程、技术员工的招聘、合作厂 商的联系、生产部门(开发部,产品部,QA部)的日常管理。
在仙童数码,他开始将自己的理念灌注其中,建立规范,公司的部门架构设立了之后,每个职位都准备一份工作职责范围,日常工作内容和工作规范。把如何做好该工作岗位,如何书写工作报告,如何制订工作计划等等的内容都通过规范文档详细做好。他强调是的团队作战能力,市场部及产品部携手负责产品开发的前期准备工作,开发部准备需求分析、技术可行性分析和系统分析文档,然后提供工作文档给质检部和产品部。开发部负责程序编写,质检部准备测试文档,产品部设计网站界面,技术支持准备维护手册。编码和测试交替进行,当产品质量达到要求后,才发布新的软件产品。研究部门对目前潮流的新技术进行深入研究,并编写技术可行性报告和制作一些软件样品,最后上交给公司的管理层进行决策。
转眼之间一年过去了,FishMan仍然在寻求着突破自我,看着自己一手带大的仙童数码更加不断的成熟和发展,他的内心也充满着另外一种渴望。FishMan卖掉了自己在仙童数码的股份,转到了同样是自已一手参与创建起来的从事网络游戏开发的天夏科技有限公司并担任CEO。
在Fishman眼中,CEO不仅仅是一种称谓,而是同国际企业接轨的一种称呼,做CEO,不是一个名字的改变,而是从整个软件公司的具体运作上要按照国际标准。走出国门,才能真正了解国外的现状,才能做一个真正的CEO。他认为在这方面,其实也适用于程序员,虽然只要是编程,无论多么偏僻遥远,都可以称为程序员,但如果能够真正在技术上和国外接轨,从软件开发规范上学习国外的标准。
Fishman曾经说过:“网络给了我们很多的机会,但是一直不敢去涉足真正的网络公司,他总觉得软件公司更真实,做技术更保险,更加适合自己,无法放弃对软件开发的情结。但是他还是非常感激网络的,网络给了软件更多的机会,无论是个人还是软件产业。”他还是无法完全放下程序员对技术的钟情。 程序员关心的是软件工程,技术方案,编程技巧和协同工作等等,目标是做个技术高手。压力来自技术的推陈出新,技术难题的解决,保证程序的质量和开发进度等等。这些都是可以通过自身的努力解决的,也就是说程序员自己可以把握的。管理者关心的是软件工程与质量控制,技术壁垒,市场切入点,成本控制,市场推广,销售渠道,政府政策,版权保护,吸纳人才,售后服务等等,目标是让公司获取收益,得到生存和发展的空间。他的压力来自公司的目标和发展机会,需要协调公司内部的运转和外部的关系。特别是公司小的时候抗风险能力很差,CEO要协调公司的资金,人员,市场和定位的矛盾。不容易把握,努力了也不一定有所改善,一不小心就会翻船。两者的转变是从局部考虑问题到全局考虑问题。一个成功是程序员-->项目主管-->开发经理-->CTO-->CEO这么过渡过来的。
虽然已经取得了很多成绩,但FishMan一直也不敢放松,对于自己一路拼搏的征程,感叹的说道:“蓦然回首,投身于中国软件行业已经五年了。我曾经满腔热血,以不眠不休的编程为乐;曾经意气风发,登上银光闪耀的领奖台;曾经深受打击,想漂洋过海逃避失意;也曾经峰回路转,再次创业风起云涌。一切的一切,岂是言语所能表达。在饱经风霜之后,不变的只有对软件产业的信念!”
使用MIDP(Mobile Information Device Profile)的开发人员经常会抱怨用些什么办法才可以在一个MIDlet上显示动画。MIDP 1.0 没有直接提供对动画的支持(正在开发中的MIDP 2.0支持),但真要是自己去实现,其实也并非是一件很难的事。
任何动画的最基本的前提,是要在足够快的时间内显示和更换一张张的图片,让人的眼睛看到动的画面效果。图片必须按照顺序画出来。从一张图片到下一张图片之间的变化越小,效果会越好。
首先要做的,是使用你的图片处理软件(比如ps或者firework)创建一系列相同大小的图片来组成动画。每张图片代表动画一帧。
你需要制作一定数量的祯--越多的帧会让你的动画看上去越平滑。制作好的图片一定要保存成PNG(Portable Network Graphics) 格式,MIDP唯一支持的图片格式;(有两个办法让你刚做好的图片在MIDlet上变成动画。第一,把图片都放到一个web服务器上,让MIDlet下载他们,MIDP内置的HTTP支持。第二个办法更简单,把图片用MIDlet打包成jar文件。如果你使用的是J2ME开发工具,把PNG文件放在你的项目文件里面就可以了。
动画的过程其实更像帐本记录:显示当前帧,然后适当地更换到下一帧。那么使用一个类来完成这个工作应该是很恰当的,那好,我们就先定义一个AnimatedImage类:
import java.util.*;
import javax.microedition.lcdui.*;
// 定义了一个动画,该动画其实只是一系列相同大小的图片
// 轮流显示,然后模拟出的动画
public class AnimatedImage extends TimerTask {;
private Canvas canvas;
private Image[] images;
private int[][] clipList;
private int current;
private int x;
private int y;
private int w;
private int h;
// Construct an animation with no canvas.
public AnimatedImage( Image[] images ){;
this( null, images, null );
};
// Construct an animation with a null clip list.
public AnimatedImage( Canvas canvas, Image[]
images ){; this( canvas, images, null );
};
// Construct an animation. The canvas can be null,
// but if not null then a repaint will be triggered
// on it each time the image changes due to a timer
// event. If a clip list is specified, the image is
// drawn multiple times, each time with a different
// clip rectangle, to simulate transparent parts.
public AnimatedImage( Canvas canvas, Image[] images,
int[][] clipList ){;
this.canvas = canvas;
this.images = images;
this.clipList = clipList;
if( images != null && clipList != null ){;
if( clipList.length < images.length ){;
throw new IllegalArgumentException();
};
};
if( images != null && images.length > 0 ){;
w = images[0].getWidth();
h = images[0].getHeight();
};
};
// Move to the next frame, wrapping if necessary.
public void advance( boolean repaint ){;
if( ++current >= images.length ){;
current = 0;
};
if( repaint && canvas != null && canvas.isShown()
){;
canvas.repaint( x, y, w, h );
canvas.serviceRepaints();
};
};
// Draw the current image in the animation. If
// no clip list, just a simple copy, otherwise
// set the clipping rectangle accordingly and
// draw the image multiple times.
public void draw( Graphics g ){;
if( w == 0 || h == 0 ) return;
int which = current;
if( clipList == null || clipList[which] == null
){;
g.drawImage( images[which], x, y,
g.TOP | g.LEFT );
}; else {;
int cx = g.getClipX();
int cy = g.getClipY();
int cw = g.getClipWidth();
int ch = g.getClipHeight();
int[] list = clipList[which];
for( int i = 0; i + 3 <= list.length; i +=
4 ){;
g.setClip( x + list[0], y + list[1],
list[2], list[3] );
g.drawImage( images[which], x, y,
g.TOP | g.LEFT );
};
g.setClip( cx, cy, cw, ch );
};
};
// Moves the animation's top left corner.
public void move( int x, int y ){;
this.x = x;
this.y = y;
};
// Invoked by the timer. Advances to the next frame
// and causes a repaint if a canvas is specified.
public void run(){;
if( w == 0 || h == 0 ) return;
advance( true );
};
};
游戏策划
(转贴自http://www.gamemaker.com.cn)
1.什么是游戏策划?
我们今天的主题是游戏策划。因此,我们应该首先明白一个问题,什么是游戏策划?曾经有朋友拿着自己的一些故事来找我们,觉得自己的故事非常感人,如果作出游戏来会非常轰动。如此说来,写一些感人的故事是否是游戏策划?就我们自身的例子来说,我们感到一个好的故事离一个好的游戏相差实在是太远了。我们的《水浒英雄传—火之魂》是根据中国古典名著改编的,为了融合现代人的观点和口味,对原著的改编非常之大,就我们本身的故事脚本而言,可以说看了都说好。但做成游戏后就是另一回事了。且不说玩家的评论,单说我们自己都感到没有完全表达出脚本的内涵。并非用好的原料就能作出一道好菜。
这不是说我们的制作人员的水平问题,而实实在在是人与人之间的思维和感受的不同所造成的。对一个故事而言,并非所有人都有同样的感受和评论。即使是一部电视剧,不同的人就会有不同的评论。就更不用说将一个故事给了七、八个甚至更多的不同个性不同思想的人,让他们做出一个让成千上万人欣赏的故事,其结果是可想而知的了。讲这些的目的是使有兴趣于此的朋友注意一点----一个故事对一个游戏而言,只是万里长征的第一步。
当然,有一些游戏根本就与故事扯不上边,如模拟类、体育类游戏大多于此。还有一些朋友拿着一厚沓脚本来与我们商量,把他们的脚本制作出来。在他们的脚本中有详细的故事、菜单设计、游戏规则设计,表面上非常全面。他们管这个叫做游戏策划。也就是说他们认为写了一个故事,设计了一些菜单的功能,制订了一些游戏的规则,就是游戏策划。如果真的是这样,事情就变得简单多了。
我相信,看这篇文章的朋友都抱着各种心态----好奇!看一看游戏制作的内幕!或者是自己想尝试一下。别人的游戏玩多了,总觉得自己有一些好的想法,绝对能策划个好游戏。但如果你真是想策划一个游戏,并且真是想有人能够制作出来的话,那么,我们有一个非常重要的前题是----谁来做这个游戏!!!你千万不要指望不顾程序和美术部分,而将故事、菜单、规则制订下来之后,你就万事大吉了。如果那样的话,你肯定得不到自己的作品,当程序和美术人员完成了工作的时候,拿给你的东西将与你想的完全不一样。你会看到----那根本不是同一个游戏。反复说这些话的原因,就是想告诉对此感兴趣的朋友们,千万不要以为游戏策划只是一个编故事、制订规则。那么作为一个游戏策划人,应该具备什么样的素质呢?什么样的人才有资格成为游戏策划呢/
2.游戏策划人应该具备什么条件?
怎么样?被我前面的话吓住了吧?这也不是,那也不是。什么样的人才是游戏策划人呢?
现在我再告诉你,一个理想的游戏策划人应该具备如下条件:
A.玩的游戏足够多,并且对各个游戏有过一番研究。因为只有这样,才能对自己的游戏进行充分的设计,才能扬长避短。也只有玩过足够多的游戏才能明白玩家的一些约定俗成的东西,比如YES、NO按钮谁在左面?谁在右面?如果反了,有些玩家会经常误操作。
B. 对程序的制作十分精通,精通游戏图形和音乐编程及人工智能。这样,你就能充分地发挥程序的作用,在不增加工作量的情况下,使游戏变得更加有趣。比如说在场景中有满街乱走的小人儿,如果不考虑更多的人工智能,这些人只是简单地随机乱走的话,一百个人和两个人几乎是用相同程序处理的。但如果两个人情况下,其中有一个人在一定的条件下,需要去一次酒馆的话,给程序方面增加的工作量,比增加一百人可大多了。而为游戏最后的测试又增加了许多工作。你必须创造必要的条件,看他去不去酒馆。而如果这个条件是随机出现的话,你不走运的话,可能会为此等上一天。
C.对美术部分十分精通,精通各种平面、三维和动画软件,具有非常扎实的美术基础。并对摄影和镜头剪接有充分的了解。如果你真的具备这一点,你就能使参与游戏的其他美术人员充分了解游戏的整体感觉。你可以告诉他们整个游戏的色调是偏暖或是偏冷。如果是卡通的话,是采用迪斯尼的卡通风格还是日本的卡通漫画家的风格?你可以亲自设计出游戏的主要角色的人物形象定位和场景效果图,这样有经验的美术人员会充分体会你所要表现的游戏气氛。是欢快活泼的趣味风格,还是阴风阵阵的恐怖风格。
D.对音乐十分精通,精通乐理并擅长古今中外各种风格的音乐。由于人们对音乐有着极强的感受和理解能力,所以一个有气氛的场景应该配合相同气氛的音乐。或是利用一种特殊的音乐效果表达特殊的气氛。我们曾经试图使一个音乐制作人明白我们为什么想将中国古典音乐与现代摇滚音乐相结合,但最后终于没有成功。
E. 有深厚的文学功底和丰富的中外历史知识,最好也能明白一些表达心理学方面的知识。这样你就能设计出动人的角色对话----表现动人情节的一个最重要的法宝,也有助于你烘托游戏时代背景。如果想设计科幻背景的游戏,你还必须熟悉未来科技的一些知识和趋势。
也许这是一个游戏策划的标准人才,但这样的人才我们从来还没有遇到过。因为一个人的一生是有限的,在有限的时间里不可能在各个方面都能得到充分的发展。没办法,只能依靠我们自己来解决了。幸好我们也许在上述几点的其中一点非常突出。那么就让象你我这样的人来策划游戏吧。但我们能够成功,因为我们拥有两件必胜法宝----谦虚好学和不懈的努力。现在让我们开始吧。
3.如何开始一个游戏策划?
开始一个游戏的策划,某种意义上说就是开始一个游戏的制作过程。首先,你必须明白一点,你的时间是有限的。如果你想成为一个专业人员,你时刻应该记住的事情是你正在制作的是一个商品,它有成本,面临蚀本的风险。而降低成本降低风险的最有效的方法就是牢牢控制住时间,在策划阶段就要充分考虑到这点。如果你是一个业余人员的话,你也不要忘记,如果你花上两年时间想做出一个好游戏的话,两年后,你的游戏可能已经烂得不成了。这样的例子真是太多了。更不用说,因为你自己的精力和时间所限,使你不得不半途而废。所以在一开始你必须有一个计划和目标,游戏的完成时间八个月?十二个月?十八个月?总之,你必须有一个明确的时间表,在这个时间表的基础上,你也许不得不抛弃或改变一些想法。
现在我们开始吧,策划游戏需要一支笔(最好是铅笔)、一沓草稿纸,如果需要的话还要一个计算器。
我们重玩了很多西方的模拟建设类游戏之后,突然产生了一个想法,如果游戏中的房屋全是我们自己的民族建筑会怎么样?那么我们就有了一个游戏的最基本的元素。
★在一个也许是沙漠、也许是草原的空场上建造民族特色的房屋。
下面我们需要为玩家设置一些障碍和奖励,这是使得游戏使人动脑筋的重要手段之一。顺便说一下,并非所有的玩家都喜欢动脑筋。但你永远不要指望一个游戏能满足所有玩家的喜好。
游戏的历史背景是第二个问题。这个问题十分容易解决,我们现在所在的北京就是一个十分合适不过的地理和历史背景了。建设古代的帝都北京。一个十分吸引人的题材不是么?我们有了游戏的第二个元素。
★以北京建都作为自己的历史背景。
建筑需要金钱、人力、物力和时间,玩家必须想方设法地得到这些必要的基础才能获得最后的成功。这就形成了游戏的第三个基本元素。
★玩家必须获得足够的金钱、人力、为人力所准备的粮食、建筑材料。
下面我们组合这些基本元素使它们成为一个比较完整的体系。这个体系基本如下:将建筑进行分类,分别用于获得金钱、人口、粮食、建筑材料等等。这些有特定功能的建筑最后分别被定义为商号、民居、粮站、官府。从商号的经营活动中,通过税收的手段获得金钱是一个很自然的事。而人口都是从民居中产生的也是一个不假思索的选择。粮站的概念则完全取决于历史背景,古代的用粮一直是依靠从南方通过水路和陆路运输而来的,所以我们可能不用考虑种地的问题了,但由此引发了一个新的元素----道路和交通,于是我们又增加了两个设施:道路和运河。至于官府取得建筑材料,主要是因为古代皇宫的建筑材料主要是通过官府征集而来的。
为了表现古代北京的城市概念,我们的建筑中又加入了庙宇、戏楼等等许多附属建筑;为了表现古代北京缺水的特点,我们又增加了水的因素和获得水的建筑----水井。
好像一个比较完整的游戏规则的雏形产生了。但在你想继续之前,你必须回答以下几个问题。
4.其它的问题
首先一个问题是你的场景或者称为地图有多大?这关系到以下问题:
A.在有限的场景中你只能放置有限的建筑,而有限的建筑中有多少是商号呢?
B. 用这么多的商号能用多少时间产生多少金钱?用这些金钱你能建造多少建筑呢?
C.多少钱一个建筑,而多长时间可以把地图上盖满建筑呢?
D. 每个建筑的尺寸是多大呢?
你可能没有想到的是,问题答案的一部分是从你的游戏最终用户的机器配置得来的。如果你问程序员,他会告诉你,一张640X480X256色的图占用内存中的尺寸是300K。计算方法是640X480/1024(1K)=300。而一张同样大小的24BIT真彩的图在内存中则占用900K的空间,正好是300K的三倍。如果你的目标运行环境是486/16M的话,显然DOS系统是目标操作系统。在16M内存中,除了程序的运行部分和数据部分可能占3-4M空间外,即使其它地方都被图形占满,你也只能放大约12个640X480X24BIT大小的图。这意味着只有3X4个全屏的大小,太可怜了。你可以用一张1920X1920大小的图感受一下。
如果你用了硬盘作为虚拟内存的技术,可能会加大场景,但运行速度可能会受到相当大的影响。
如果考虑采用256色的话,你的美术人员必须面对一个更加严峻的问题----调色板,这是一个令无数英雄豪杰头痛不已的问题。它不但意味着图形质量的严重下降,还意味着工作量的成倍增加。可能你没有直接的感受,但是我们可以告诉你,过去在DOS环境下开发游戏,大约有60%的时间在解决调色板的问题。
指望用户在WIN95下运行游戏是你的明智选择。这样用户的机器配置也会随之提高。同时由于WIN95的虚拟内存技术和更快的硬盘驱动程序。你的场景会大大提高。但还是有着方方面面的因素在影响你的选择。
当场景的大小定下来后,你就可以制订一些参数了。首先你需要合理的设计出一个城市,所谓合理是指使绝大多数玩家不感到别扭。然后,再找出其中有多少是商业建筑,多少是民居,多少是官府。在此基础上再确定建造一个城市需要的金钱数,你需要给整个城市中的每一种建筑制订价格。然后再与其它因素一起计算出需要的金钱,判断一下玩家建造一个城市能忍耐的时间,太短会有不过瘾的感觉,太长就会将你的游戏束之高阁。然后,你就给你的建筑制订等级,并确定不同等级建筑的效能。总的来说,就是保证各种等级之间的平衡。比如,将计算出的金钱数分配给不同等级的商号。在分配过程中,你可以通过参数的调整引导用户建造更多的某个等级的商号。你可以在攻略中或手册中说明哪种等级的商号更加合算。
象这样使你头晕的问题非常之多,你都要一一去面对并加以解决。你可能会说,这样不是已经进入游戏的制作过程了吗?其实不是,当这些参数被真正在游戏中体现出来的时候,你就会发现,有许多你没有意想到的东西。但即便如此,你也需要在具体实施制作之前,在你的脑海中,就象真正在玩这个游戏一样,将这个游戏的设计模拟一遍又一遍。以期从中发现问题。然后再分割成美术和程序人员都看得懂的脚本,再分别交给美术和程序人员进行制作。
你看,游戏策划有的时候更需要的是细致和耐心,需要对程序和美术有一定的了解。并不断地与他人进行沟通。
RPG游戏公式
1、很久以前,在一个遥远的地方,有一班勇者,一个魔王,一堆虾 兵蟹将,一众可有可无的村长和村民,以及一个感人(?)的故事。
2、所谓的勇者,一开始比任何生物都弱,连一只鸟或单细胞生物都打不赢。
3、魔王有村庄不去住,只喜欢建立一个黑暗复杂的迷宫把自己关起来,一动不动地等勇者来。
4、魔王会派出虾兵蟹将对付勇者,但为了使勇者能轻易应付, 会根据勇者的等级派出不太强又不太弱的对手,偶然有一些太强的对手以致勇者打不赢,便一定有另一段偶然的故事救了勇者一命。
5、魔王令他的虾兵蟹将将一些优质装备,道具及钱带在身上, 却又不许使用,等勇者打败他们后来自用。
6、魔王在自己住的迷宫里四处弃置宝箱,为勇者提供足够的装 备和道具,以确保勇者能见到魔王。
7、魔王传令:如勇者逃跑,绝不能追杀,更加不能随便行动,不得上下楼梯,不得擅自开门。
8、最后,魔王被勇者打败了,有了以下结局:
版本一:勇者杀死魔王,世界回复和平。
版本二:勇者打败魔王,王子和公主便过着快乐的生活。
版本三:魔王大叫:可恨啊……我会再回来的……然后消失了, (请期待下个故事)。
版本四:一阵强光过后,勇者和魔王一起消失了,只留下勇者的剑。
版本五:女主角与魔王同归于尽或女主角牺牲的所谓永恒的悲剧。
版本六:这次的勇者变了魔王,下次请打他吧。
版本七:有个傻瓜梦醒了。
版本八:——THE END—— 一堆字幕
版本九:C:>
图块象素化的边缘抗锯齿处理
由于通常用作游戏画面的人物肖像、背景等需要做扣图处理的图片在处理前底色都是白色,如果直接这样的话,很可能做出来的象素化后的图片(在256色的环境下,整个图块颜色少于40色)会有明显的边缘锯齿。
克服锯齿或者减弱锯齿效应以前使用的方法是修边,即用深色,例如纯黑,把图块的周围描绘一遍,这样做可以很好的减少锯齿并能提高象素的味道,但是缺点有:对于极细的线条的处理,例如一条线只有1个象素宽,那么描边过后就有3象素宽,这在某些场合是不可取的,很可能会破坏原由画面的味道。而对于手机上的图象处理,这样的结果肯定不行。
那么,就只好进行没有描边的处理方式。
查看了日本最新格斗游戏kof2003的图片内容,kof系列的人物从来都是不描边的,即完全用色块来表现角色。这对于32*32尺寸以内的小图片来说,直接在方格内照原画用象素画法画一遍就可以了,但是如果尺寸是大于32*32的或者战局大半个手机屏幕的一幅图呢?例如人物的特写,rpg中会用到的头像?这个时候如果还用象素的方式来描绘很可能会跟原画有相当大的出入,而且修改起来也很麻烦。
因此,可以利用原画来进行加工!
把底色设置为蓝色(0,0,255)然后缩小图片尺寸,这样图块的周围一圈就很好的和兰色融合在了一起,然后通过反复的在索引色及rgb色之间的转换减少原色,同时对一些相邻色进行删减(把相邻的颜色用灌桶填成同一种颜色,例如121,34,133与121,34,144就可以合成为一个颜色)。这样最终做出来的图片就是以兰色为底色的颜色很少但失真并不严重的图片,把加工的图块扣取出来,放到背景里面,发现融合程度很好,边缘锯齿弱化很多。
这是以白色为幕布做出来的图片----锯齿非常明显。
将完成的原画背静改为兰色------是这样样子滴!
将上图减小尺寸过后------就是这个样子滴!
此时再用你所能想到的任何方法,在不明显失真的前提下进行颜色压缩,最后把兰色去掉,就得到成品啦。哈哈啊。
用另外一个女生的图片做的图
同样的方法,可以取得同样良好的结果。
反复进行不同背静的测试,
发现锯齿问题解决得很好,兰色的背静幕布在抠图过后能与其他颜色很好的融合。
动画方面.
前面的朋友说了动画的制作,一个动画是由很多帧组在的.,您说的方法把一帧帧的动画做成一幅幅的图象,再一幅幅地载入,这样做的效率不高.应该把各幅员的动画放在一起,做成一幅大的图,加载一幅大图比加载多幅小图效率要高,一幅大图也比多幅小图容易管理得多.那么怎样做呢?
做法和前面朋友说的差不多,关键是这个方法:
drawImage(Image img,dx1,dy1,dx2,dy2,sx1,sy1,sx1,sy1,this)
其中Image img是来源图象,
dx1是目的区域的===左上===顶点的===X====坐标;
dy1是目的区域的===左上===顶点的===Y====坐标;
dx2是目的区域的===右下===顶点的===X====坐标;
dy2是目的区域的===右下===顶点的===Y====坐标;
sx1是来源区域的===左上===顶点的===X====坐标;
sy1是来源区域的===左上===顶点的===Y====坐标;
sx2是来源区域的===右下===顶点的===X====坐标;
sy2是来源区域的===右下===顶点的===Y====坐标.
================================================================
下面是一个动画的程序代码.
import java.awt.*;
import java.applet.*;
public class Seriallmage4 extends Applet implements Runnable
{
int SPF; //用来表示线程暂停的时间
int sx,sy;
/******************************************************************
sx,sy用来表示来源区域的左上顶点.因为每一帧我们都做成了长是80宽87的图,再把它们放在一起做成一幅大的图,
所以来源区域的右下顶点的坐标等于左上顶点坐标加上长和高.
式子为:sx+80;sy+87
******************************************************************/
Image Animation; //来源图象
MediaTracker MT; //图像追踪器,用来检查图像是否完全加载
Thread newThread; //新的线程.
Image OffScreen; //后台图像;
Graphics drawOffScreen; //绘制后台图像的实体(实例).
//======================init()函数===========================
public void init()
{
SPF=100; //线程暂停的时间是100豪秒,
MT=new MediaTracker(this); //实体化图像追踪器.
Animation=getImage(getDocumentBase(),"lunisEx.gif");//加载图像.
//======================//追踪图像=============================
MT.addImage(Animation,0);
try
{
showStatus("图象加载中...."); //在状态栏中显示信息
MT.waitForAll(); //等待加载
}
catch(InterruptedException E){}
//============================================================
OffScreen=createImage(300,300); //建立后台画面的实体
drawOffScreen=OffScreen.getGraphics(); //取得后台画面的绘制类
}
//===========================================================
public void start() //start函数
{
newThread=new Thread(this); //建立线程
newThread.start(); //启动线程
}
public void stop() //stop函数
{
newThread=null; //线程设为null
}
//==========================================================
public void paint(Graphics g)
{
drawOffScreen.setColor(Color.green); //设置后台画面的背景色,
drawOffScreen.fillRect(0,0,300,300); //画一次背景,作用是清除后台画面,如果背景是一幅画
//则要用这幅图来画.
drawOffScreen.drawImage(Animation,20,20,250,250,sx,sy,sx+80,sy+87,this);//在后台画面绘制
//的图像,
g.drawImage(OffScreen,0,0,300,300,this);//把后台画面画到前台画面
}
public void update(Graphics g)
{
paint(g);
}
//====================新动画循环在此========================50
public void run()
{
while(newThread !=null) //如果线程不等于null
{
repaint(); // 重画一次.
try
{
Thread.sleep(SPF); //让线程暂停
sx+=80; //画了一次后,改变来源区域,使其到下一帧.
if(sx==800) //我的图像是一幅800*170的,有两行,每行十帧,如果sx走到了
//800就说明播放完第一行了,
{
sy+=85; //播放完第一行后,改变来源区域的左上顶点使其跑到第二行.
sx=0;
if(sy==170) //如果播放完第二行后,又让它回到第一行,
{
sy=0;
}
}
}
catch(InterruptedException E){}
}
}
}
动画每秒播放多少帧好呢?
电影的是每秒24帧,这样的速度不仅能产生视觉停留,而且让人感觉到画面非常流畅.那么是不是我们做游戏也要用这个速度呢?答案是否定的,实际上每秒播放10帧就足已产生视觉停留,如果设置高了会消耗更多的资源.至命的是,有一些手机连每秒10帖都做不到.
游戏开发者的成功秘诀
“离子风暴”开发公司的奠基人之一汤姆·霍尔先生在网上发表了一篇有趣的文章,披露了快速成为游戏开发者的“成功秘诀”。唉,世上哪有那么便宜的事呢?可汤姆·霍尔说得一本正经,一脸严肃,不由得你不信。再加上笔者生来对“投机取巧”“不劳而获”“一蹴而就”之类的事情有一种按捺不住的好奇(首先声明,笔者一生的道路走得踏踏实实,一步一个脚印,绝无此类“劣迹”,仅好奇耳),便偷偷地看了。不看则已,一看才知世上竟有汤姆·霍尔这类大滑头,失望之余便有些忿忿然,忿忿然之余又不禁哑然失笑,豁然明白了霍尔先生的苦心。原来他出此奇文并非存心戏弄读者,实在是有几分“劝世”的用意在其中。
作为闻名遐迩的游戏开发者,汤姆·霍尔在开发游戏的同时(眼下他正忙着开发一款以未来为背景的游戏:《时空异变》Anachronox),常常会收到一些来信,写信者表露出有心在游戏业驰骋一番的雄心,提出的问题可大致划分为以下三种:
1、“我脑子里新奇念头层出不穷,你愿意雇用我吗?”
对于第一种来信,霍尔的回答只有两句话:“新奇念头人人有”和“不行”。霍尔解释说:“如今游戏业并不缺乏新奇念头,每天都有成千上万的新念头在产生和消亡。尽管新奇对于一个游戏的成败至关重要,然而更重要的是行动,将其付诸实现。爱迪生说什么来着:‘发明是百分之一的灵感和百分之九十九的汗水的混合物。’除非你拿出东西来,否则没人愿意听你凭空说话。埋头制作游戏需要某种献身精神,你可能会嘲笑那些开发游戏的人‘这也做得不好’、‘那也做得不对’,可他们实实在在完成了一件作品,这就使他们显得与众不同。因为99%的人是希望在他们付出劳动之前能确保得到回报的庸常之辈。”
2、“我把自己刚刚完成的游戏设计寄来,你能抽空看看吗?很了不起的构想噢!”
对于这种来信,霍尔拒绝得更快,几乎是不加思索的“No”。原因也有些出人意表:“他们寄来的构想可能是粗浅的习作,也可能是未来的惊世大作,可我是一眼也不敢看的。原因很简单,我自己也是搞游戏策划的,他的想法可能会与我已有的想法不谋而合。由于这种事无法证明,所以他会认为我剽窃了他的念头。第二,我看过的东西会残存在我的感知里,在我将来的游戏策划中它会不知不觉地跑出来作怪,这样我还是摆脱不了被指控剽窃的危险。第三,一旦我看过了某个构思,我就必须对其加以评论,运用我自己的想法对其进行指导。出于自私的考虑,我不想让自己的‘酷’念头为他人所用,我自己将来还用得着呢。还有呵,对于杂志上发表的有关游戏策划的评说我也是避之而唯恐不及,尽管发表意见的人会说:‘这就是我认为的好游戏,你们就照着做吧。’可一旦这个念头被某个游戏开发者所采用,并且挣了几百万,98%的人会打官司索要专利费。另外2%的人也会在配偶的撺掇下把你告上法庭。唯一的办法就是眼不见为净。”
3、“我觉得制作游戏是一件令人兴奋的事。你是怎么走上这条路的?你使用了什么工具?”
让霍尔先生看着顺眼的大概只有这第三种问题了。由于这种信件仅仅是提出问题,而没有隐含的危险性,霍尔先生的解答也就来得很爽快。在这里他不仅仅是回答问题,而是借此对游戏开发这个行当表述了自己独特的见解。
“倘若你真的喜欢电脑游戏,而你周围又没人会开发游戏,而你自己又没有通过书本和工具学会开发游戏的能力,除非有人教你如何开发游戏否则你便不知该怎样着手,倘若情况是这样,那你就趁早打消开发游戏的念头。
“倘若你刚刚玩过一款伟大的游戏便觉得自己也能行,而实际上你对游戏开发一无所知,你所持有的仅仅是对成功和名望的渴望,那你就不要做游戏开发者。
“倘若你无可救药地迷上了电脑游戏,如醉如痴地阅读游戏杂志,时不时地用QuickBASIC编写自己的简单游戏,不畏艰难地将一些好游戏的内核打开,来回比较它们的优劣,那你也不适合搞游戏开发。”
汤姆·霍尔不留情面地把以上几种人都“炒了鱿鱼”,因为在他看来,游戏业不是个仅靠热情和痴迷便能胜任的行当。游戏公司是赚钱的公司,而不是搞游戏研究的地方。这里的竞争法则是严酷的,一如好莱坞电影业。在投奔这个新世界之前你要考虑再三,并且要准备在开始阶段去饭馆端碟子——可能会端上一辈子。
“倘若以上所说的仍难不住你,你仍旧无法打消开发游戏的念头,那么我就给你详细描述一下你将面临的艰难道路,这是一条可能走向成功的充满荆棘的道路。”看来霍尔先生终于不忍心将那么多痴迷的年轻人拒之门外,开始布道施教了。“在你充分了解了它的艰难之后,我将向你指出另一条路,我会把不必花费多少力气便能快速成功的超级秘诀告诉你。不过要在你阅读了下面的内容之后。”
打开你的电脑,在DOS状态下敲入QBASIC,你会发现有一个程序在运行。它就是QuickBASIC,随着DOS附送的一个编程程序。你的操作系统种没有一样东西是用它编写的——它运行太慢——但你却可以用它来写一些游戏程序。依照霍尔先生的看法,你应该先从学习别人的程序着手。上街买一些游戏编程手册,然后就可以干起来了。你会犯这样或那样的错误,然而没人会在意的。在“观摩学习”中,你要留意几个问题:他们是如何使游戏变得有趣而使操作变得简单的?倘若换了自己会怎么做?他们是如何改进的?什么东西使你在玩了50个小时之后依然不对它产生厌烦?
汤姆·霍尔布置的作业可不轻松,大约需要开发100个左右自己的小游戏。前10个应该是不堪入目的入门习作,从第11个开始,应该有了一点起色,从第50个游戏起,你开始有了自己的声音。正如音乐家的成长一样,在你开发了100个游戏之后,游戏开发程序就成了可以让你任意支配的乐器,用它来随心所欲地表达自己的思想了。
你要开发的游戏种类应该没有限制,想到什么就开发什么。作为开始,你不妨拿游戏业最老的游戏之一 Pac-Man来作临本,你会发现它实际上不象看上去那么简单。你还可以让一个小人从 A点走到 B点,躲避从 C点掉下来的一个小球。关键是你要开始制作游戏,并完成它。随着自己的作品的增多,你会产生自豪感,并且会逐渐感到上瘾。等到某一天,你的朋友要求拷贝一份你制作的游戏回家去玩,那时你差不多就出徒了。你再绞尽脑汁制作一个全新的游戏,一个前人从未干过的新品,尽可能把它打磨精细,然后就可以拿到游戏业界去闯运气了。
成功的道路不止一条,然而汤姆·霍尔走向成功的道路却是实实在在的先例,想成为游戏开发者的人自然很想知道。我们将他走过的道路粗略地画了一个轮廓:
从小常去玩街机,后来自己买了个游戏机,上高中时买了电脑,自己开发了50个游戏。而后上大学学习编程,又利用业余时间制作了30个游戏,其它的业余时间则全部用来玩游戏。阅读有关书籍与杂志的同时,开始为杂志撰稿,还为有智力障碍的学生编制游戏,毕业时拿到了计算机专业学位,在一个小公司谋得职业,它每月都有程序发布。在搞应用程序的同时又制作了几款游戏,并开始在其它一些制作游戏的部门窥探,结识了几个游戏天才人物,并使自己的作品得到他们的赏识。等到羽毛丰满,便自立门户,创建了自己的游戏公司。每天工作16个小时,每星期工作7天,挣的钱仅够维持生计。制作各种各样的游戏,逐步走向成功。这条道路是艰难和漫长的,然而要想成为一个游戏开发者,该做的事一样少不了。
“不是说还有一条道路吗?”心有不甘的读者问。
不错,的确还有一条路。走这条路你可以不必花费时间和精力去学习编程,你可以在一夜之间实现自己的梦想。汤姆·霍尔知道这条路,并把它称为“走向成功的超级秘诀”,慷慨地在此奉献给大家。秘诀分为三个步骤,排列如下:
第一步:有几百万美金闲钱可调用。
第二步:购买一个现成的游戏开发公司。
第三步:别忘了在合同里加上一条,公司开发的所有游戏前面都要加上你的大名。
大功告成!这样你就(至少在公众的眼里)成了一位游戏开发者。是不是简洁而又迅速?看着游戏封面你那烫金的大名,你是否感到陶醉?唯一的不足之处就是,你别指望这样别人会对你产生多少敬意。假如你真的想成为货真价实的游戏开发者,除了第一条充满荆棘的困难道路外,似乎没有什么别的路好走。
本文通过使用J2ME开发华容道游戏,介绍了J2ME游戏开发的基本模式....
一、序言
昨天在网上闲逛,发现一篇讲解用delphi实现华容道游戏的文章,颇受启发.于是,产生了将华容道游戏移植到手机中去的冲动.现在手机游戏琳琅满目,不一而足,华容道的实现版本也很多.正巧不久前笔者对J2ME下了一番功夫,正想借这个机会小试牛刀。选用J2ME的原因还有一个就是目前Java开发大行其到,无线增殖业务迅猛发展,J2ME的应用日渐活跃起来,也希望我的这篇文章能够为J2ME知识的普及和开发团队的壮大推波助澜。由于长期受ISO规范的影响,这次小试牛刀我也打算遵照软件工程的要求,并采取瀑布式的开发模式来规划项目,也希望借此机会向各位没有机会参与正式项目开发的读者介绍一下软件开发的流程。
这里我们先定义项目组的人员体制(其实只有我一个人):技术调研、需求分析、概要设计、详细设计、编码、测试均有笔者一人担任;美工这里我找了个捷径,盗用网上现成的图片,然后用ACDSee把它由BMP转换成PNG格式(我出于讲座的目的,未做商业应用,应该不算侵权吧);至于发布工作,由于缺少OTA服务器,此项工作不做(但是我会介绍这步如何做)。
接下来,我们规划一下项目实现的时间表,以我个人经验,设想如下:技术调研用2天(这部分解决项目的可行性和重大技术问题,时间会长一些),需求分析用半天(毕竟有现成的东东可以参照,只要理清思路就行了,况且还有很多以前用过的设计模式和写好的代码),概要设计再用半天(有了需求,概要只不够是照方抓药),详细设计要用2天(这一步要把所有的问题想清楚,还要尽可能的准确描述出来),编码用2天(其实1天就够了,技术已经不是问题,多计划出一天来应付突发事件),测试用2天(测试应该至少占全部项目的四分之一,不过这个项目只是一个Demo,也太简单了),发布也要用上半天(尽管我们不去实际发布它,但是还要花点时间搞清楚应该如何做),最后就是项目总结和开庆功会(时间待定)。
二.利其器
“公欲善其事,必先利其器”,做项目之前第一步是前期调研.我们要做的华容道这个东东随处可见,我们要调研的是两个方面:
1、游戏的内容:游戏本身很简单,就是有几个格子,曹操占据其中一个较大的格子,然后被几个格子包围,这些格子形状不一定相同,但是挡住了曹操移动的方向.游戏者需要挪动这些格子最终把曹操移动到一个指定的位置才算是过关.更具体的分析我们放在后面需求分析和概要设计中讨论。
2、技术储备:谈到技术,这里简单介绍一下J2ME.Java有三个版本,分别是J2ME(微型版).J2SE(标准版).J2EE(企业版).J2ME是一个标准,采用3层结构设计.最低层是配置层(Configuration)也就是设备层,其上是简表层(Profile),再上是应用层(Application).MIDP就是移动信息设备简表,目前主流手机支持MIDP1.0,最新的是MIDP2.0,它比前一个版本增加了对游戏的支持,在javax.microedition.lcdui.game包中提供了一些类来处理游戏中的技术,比如我们后面会用到的Sprite类,它是用来翻转图片的.权衡再三,笔者决定使用MIDP2.0来做开发.首先需要安装一个J2ME的模拟器,我们就用Sun公司的WTK2.0,我觉得Sun的东西最权威.当然你也可以使用Nokia.Siemens或是Motolora等其他模拟器,但是他们的JDK不尽相同,写出来的程序移植是比较麻烦的.Sun公司的WTK2.0可以到<A href="http://here/下">http://here/下</A>载,当然要想成功下载的前提是你要先注册成为Sun的会员(其实这样对你是有好处的).当下来之后就是按照提示一步一步的安装.安装好了之后,我们用一个"Hello World"程序开始你的J2ME之旅.我们启动WTK2.0工具集中的KToolBar,然后点击New Project按钮,在弹出的输入框中输入Project Name为HelloWorld,MIDlet Class Name为Hello,然后点击Create Project,开始生成项目,工具会弹出MIDP配置简表,这里接受生成的默认值(以后还可以修改)点击OK,工具提示我们把写好的Java源程序放到[WTK_HOME]\apps\HelloWorld\src目录之下.我们编辑如下代码,并保存在上述目录之下,文件名为Hello.java。
import javax.microedition.midlet.*; import javax.microedition.lcdui.*; public class Hello extends MIDlet { private Display display; public Hello(){ display =Display.getDisplay(this); } public void startApp(){ TextBox t = new TextBox("Hello","Hello",256,0); display.setCurrent(t); } public void pauseApp(){ } public void destroyApp(boolean unconditional){ } } |
保存好了之后,点击Build按钮,工具会为你编译程序,如无意外再点击Run按钮,会弹出一个手机界面,剩下的就不用我教了吧(用鼠标对手机按键一顿狂点)。呵呵,你的第一个J2ME程序已经OK了.什么?你还一点都没懂呢(真是厉害,不懂都能写出J2ME程序来,果然是高手).我这里主要是介绍WTK2.0工具的使用,程序并不是目的,不懂的话后面还会有详细的解说,这里只是带你上路.什么?你不懂Java!那也没有关系,后面我再讲得细一点。
跳过J2ME,我们先来讲点游戏的理论.具体到华容道这个游戏,主要有三个方面,贴图.游戏操作.逻辑判断.这里讲讲贴图,其他两方面放在概要设计和详细设计里讲.所谓的贴图,其实就是画图,就是在要显示图形的位置上输出一副图片,(要是牵扯到动画就要麻烦一些,可以使用TimerTask.Thread或Rannable之类的技术),这副图片可以是事先准备好的也可以是临时处理的.在J2ME中有一个Image类,专门用于管理图片,它有createImage()方法,可以直接读取图片文件(J2ME只支持PNG格式的图片),也可以截取已有的图片的一部分(这样我们可以把很多图片放在一起,然后一张一张的截下来,好处是节省存储空间和文件读取时间,对于手机这两者都是性能的瓶颈).J2ME还有一个Graphics类,专门用于绘图,它有drawImage()方法,可以把一副图片在指定的位置上显示出来,它还有drawRect()方法和setColor()方法,这两个方法在后面我们进行游戏操作时就会用到,这里先交代一下.有了图片和绘图的方法,还需要知道把图画到谁身上,J2ME提供了一个Canvas类,字面意思就是画布,它有一个paint()方法用于刷新页面,还有一个repaint()方法用于调用paint()方法.听着有些糊涂是吧,不要紧,我来结合具体程序讲解一下.为了今后编程的方便,我们创建两个类Images和Draw,Images用于保存一些常量值和图片,Draw主要是用于画图,这两个类的源代码如下。
Images类的源代码如下:
package huarongroad;
import javax.microedition.lcdui.*; import javax.microedition.lcdui.game.*;
public class Images {//保存常量 //绘图位置常量 public static final int UNIT = 32;//方块的单位长度 public static final int LEFT = 10;//画图的左边界顶点 public static final int TOP = 9;//画图的上边界顶点 //地图位置常量 public static final int WIDTH = 4;//地图的宽度 public static final int HEIGHT = 5;//地图的高度 //地图标记常量 public static final byte CAOCAO = (byte) 'a'; <A href="file://曹">file://曹</A>操的地图标记 public static final byte MACHAO = (byte) 'b';//马超的地图标记 public static final byte HUANGZHONG = (byte) 'c';//黄忠的地图标记 public static final byte GUANYU = (byte) 'd';//关羽的地图标记 public static final byte ZHANGFEI = (byte) 'e';//张飞的地图标记 public static final byte ZHAOYUN = (byte) 'f';//赵云的地图标记 public static final byte ZU = (byte) 'g';//卒的地图标记 public static final byte BLANK = (byte) 'h';//空白的地图标记 public static final byte CURSOR = (byte) 'i';//光标的地图标记 //地图组合标记常量 public static final byte DLEFT = (byte) '1'; <A href="file://组">file://组</A>合图形左边标记 public static final byte DUP = (byte) '2'; <A href="file://组">file://组</A>合图形上边标记 public static final byte DLEFTUP = (byte) '3'; <A href="file://组">file://组</A>合图形左上标记 //图片常量 public static Image image_base;//基本图片 public static Image image_Zhaoyun;//赵云的图片 public static Image image_Caocao;//曹操的图片 public static Image image_Huangzhong;//黄忠的图片 public static Image image_Machao;//马超的图片 public static Image image_Guanyu;//关羽的图片 public static Image image_Zhangfei;//张飞的图片 public static Image image_Zu;//卒的图片 public static Image image_Blank;//空白的图片 public static Image image_Frame;//游戏框架的图片
public Images() {//构造函数 }
public static boolean init() {//初始化游戏中用到的图片 try { image_base = Image.createImage("/huarongroad/BITBACK.png"); image_Frame = Image.createImage(image_base, 126, 0, 145, 177, Sprite.TRANS_NONE); //Sprite类是用来翻转图片的,是MIDP2.0新新增加的支持游戏的特性 image_Zhaoyun = Image.createImage(image_base, 0, 0, UNIT, 2 * UNIT, Sprite.TRANS_NONE); image_Caocao = Image.createImage(image_base, UNIT, 0, 2 * UNIT, 2 * UNIT, Sprite.TRANS_NONE); image_Huangzhong = Image.createImage(image_base, 3 * UNIT, 0, UNIT, 2 * UNIT, Sprite.TRANS_NONE); image_Machao = Image.createImage(image_base, 0, 2 * UNIT, UNIT, 2 * UNIT, Sprite.TRANS_NONE); image_Guanyu = Image.createImage(image_base, UNIT, 2 * UNIT, 2 * UNIT, UNIT, Sprite.TRANS_NONE); image_Zhangfei = Image.createImage(image_base, 3 * UNIT, 2 * UNIT, UNIT, 2 * UNIT, Sprite.TRANS_NONE); image_Zu = Image.createImage(image_base, 0, 4 * UNIT, UNIT, UNIT, Sprite.TRANS_NONE); image_Blank = Image.createImage(image_base, 1 * UNIT, 4 * UNIT,UNIT, UNIT, Sprite.TRANS_NONE);
return true; }catch (Exception ex) { return false; } } } |
Draw类的源代码如下:
package huarongroad;
import javax.microedition.lcdui.*;
public class Draw { //绘制游戏中的图片 public Draw(Canvas canvas) {//构造函数 }
public static boolean paint(Graphics g, byte img, int x, int y) { //在地图的x,y点绘制img指定的图片 try { paint(g, img, x, y, Images.UNIT);//把地图x,y点转化成画布的绝对坐标,绘图 return true; } catch (Exception ex) { return false; } }
public static boolean paint(Graphics g, byte img, int x, int y, int unit) { try { switch (img) { case Images.CAOCAO://画曹操 //变成绝对坐标,并做调整 g.drawImage(Images.image_Caocao, Images.LEFT + x * unit, Images.TOP + y * unit, Graphics.TOP | Graphics.LEFT); break; case Images.GUANYU://画关羽 g.drawImage(Images.image_Guanyu, Images.LEFT + x * unit, Images.TOP + y * unit, Graphics.TOP | Graphics.LEFT); break; case Images.HUANGZHONG://画黄忠 g.drawImage(Images.image_Huangzhong, Images.LEFT + x * unit, Images.TOP + y * unit, Graphics.TOP | Graphics.LEFT); break; case Images.MACHAO://画马超 g.drawImage(Images.image_Machao, Images.LEFT + x * unit, Images.TOP + y * unit, Graphics.TOP | Graphics.LEFT); break; case Images.ZHANGFEI://画张飞 g.drawImage(Images.image_Zhangfei, Images.LEFT + x * unit, Images.TOP + y * unit, Graphics.TOP | Graphics.LEFT); break; case Images.ZHAOYUN://画赵云 g.drawImage(Images.image_Zhaoyun, Images.LEFT + x * unit, Images.TOP + y * unit, Graphics.TOP | Graphics.LEFT); break; case Images.ZU://画卒 g.drawImage(Images.image_Zu, Images.LEFT + x * unit, Images.TOP + y * unit, Graphics.TOP | Graphics.LEFT); break; case Images.BLANK://画空白 g.drawImage(Images.image_Blank, Images.LEFT + x * unit, Images.TOP + y * unit, Graphics.TOP | Graphics.LEFT); break; case Images.CURSOR://画光标 g.drawRect(Images.LEFT + x * unit, Images.TOP + y * unit,Images.UNIT,Images.UNIT); break; } return true; }catch (Exception ex) { return false; } } } |
其中Images类存的是绘图位置常量(也就是在画图时每个格子的长度和相对坐标原点位置要进行的调整)、地图位置常量(地图的长、宽),地图标记常量(人物对应的记号),地图组合标记常量(后面会细说),图片常量(存放人物的图片);Draw类主要负责在制定的位置画出人物图片。下面我来说说Images类中的地图标记常量和地图组合标记常量。为了能够灵活的安排各个关面的布局,我们决定把游戏布局的信息存储在外部文件中,然后程序启动后把它读进来。这样我们制定了一套存储图片的代码,这就是地图标记常量,如上面Images类中定义的Caocao(曹操)用a字符来表示,当程序读到a字符时就能将它转化成曹操对应的图片,并在读到a字符的位置上进行显示。但是从实际观察中我们发现所有的图片并不是统一大小的,有的占4个格子,有的占2个格子,还有的占1个格子,而且即便同是占两个格子的图片还有横、竖之分。有鉴于此,我们引入了地图组合标记常量,就是说在遇到占有多个格子的时候,值1(也就是Images.LEFT)表示它的左边是一个真正的地图标记,值2(也就是Images.UP)表示它的上边是一个真正的地图标记,值1(也就是Images.LEFTUP)表示它的左上边是一个真正的地图标记。地图组合标记常量其实就是用来占位置的,与实际显示无关,当后面我们将到移动时还会再来分析组合标记的使用。
Draw类主要是用来在画布上画出图形,它有两个paint方法,这是很常见的函数重载。但是程序中实际上只用到了4个参数的paint方法,它直接获得要画图片的相对坐标位置信息,然后调用5个参数的paint方法。5个参数的paint方法将相对坐标位置信息转换成绝对位置,并实际调用Graphics.drawImage()方法,将Images中的图片画了出来。这种实现方法的好处是灵活和便于扩展,但你需要画图的位置并不能够对应到格子中的相对坐标位置时,你就可以直接调用5个参数的paint方法,而不必再去修改这各类;但你添加新的图片时,只要在Images中增加对应的常量,然后向Draw中5个参数的paint方法添加一条处理就可以了。
写到这里,两天的时间刚好用完。
三、需求分析
这部分叫做需求分析,听起来挺吓人的,其实就是搞清楚我们要做什么,做成什么样,那些不做。下面我引领着大家共同来完成这一步骤。首先,我们要做一个华容道的游戏,华容道的故事这里不再赘述了,但其中的人物在这里限定一下,如上面Images类里的定义,我们这个版本只提供曹操(Caocao)、关羽(Guanyu)、张飞(Zhangfei)、赵云(Zhaoyun)、黄忠(Huangzhong)、马超(Machao)和卒(Zu)。我们这里也限定一下游戏的操作方法:首先要通过方向键选择一个要移动的区域(就是一张图片),被选择的区域用黑色方框框住;选好后按Fire键(就是确定键)将这块区域选中,被选中的区域用绿色方框框住;然后选择要移动到的区域,此时用红色方框框住被选择的区域;选好要移动到的区域之后按Fire键将要移动的区域(图片)移到要移动到的区域,并去掉绿色和红色的方框。这里需要强调的概念有选择的区域、选中的区域、要移动的区域和要移动到的区域,这四个概念请读者注意区分,当然也应当把这一部分记入数据字典之中。为了使文章的重点突出(介绍如何制作一个J2ME的收集游戏),我们这里限定一些与本主题无关的内容暂不去实现:过关之后的动画(实现时要用到TimerTask或Thread类,后续的系列文章中我会详细介绍动画方面的知识)、关面之间的切换(其实很简单,当完成任务之后重新再做一边)、暂停和保存等操作(这部分的内容介绍的资料很多,我也写不出什么新的东东来,难免抄袭,故此免掉)。
需求分析基本完成,离下午还有一段时间,马上动手用ACDSee把从网上找来的BMP文件,调整其大小为271*177(我的这个图片是两个部分合在一起,所以比手机实际屏幕大了),另存为PNG格式。半天时间刚刚好,不但搞清楚了要做的东东,还把要用的图片准备好了。
四、概要设计
概要设计是从需求分析过渡到详细设计的桥梁和纽带,这一部分中我们确定项目的实现方法和模块的划分。我们决定将整个项目分成五个部分,分别是前面介绍的Images、Draw,还有Map和Displayable1和MIDlet1。Images和Draw类功能简单、结构固定,因此很多项目我们都使用这两各类,这里直接拿来改改就能用了,前面已经介绍过这里不再赘述。Map类是用来从外部文件读入地图,然后保存在一个数组之中,这部分的内容是我们在本阶段讨论的重点。Displayable1是一个继承了Canvas类的画布,它用来处理程序的主要控制逻辑和一部分控制逻辑所需的辅助函数,主要函数应该包括用来绘图的paint()函数、用来控制操作的keyPressed()函数、用来控制选择区域的setRange()函数、用来控制选择要移动到区域的setMoveRange()函数、用来移动选中区域的Move()函数和判断是否完成任务的win()函数,更具体的分析,我们放到详细设计中去细化。MIDlet1实际上就是一个控制整个J2ME应用的控制程序,其实也没有什么可特别的,它和我们前面介绍的"Hello World"程序大同小异,这里就不展开来说了,后面会贴出它的全部代码。
Map类主要应该有一个Grid[][]的二维数组,用来存放华容道的地图,还应该有一个read_map()函数用来从外部文件读取地图内容填充Grid数据结构,再就是要有一个draw_map()函数用来把Grid数据结构中的地图内容转换成图片显示出来(当然要调用Draw类的paint方法)。说到读取外部文件,笔者知道有两种方法:一种是传统的定义一个InputStream对象,然后用getClass().getResourceAsStream()方法取得输入流,然后再从输入流中取得外部文件的内容,例如
InputStream is = getClass().getResourceAsStream("/filename"); if (is != null) { byte a = (byte) is.read(); } |
这里请注意文件名中的根路径是相对于便以后的class文件放置的位置,而不是源文件(java)。第二种方法是使用onnector.openInputStream方法,然后打开的协议是Resource,但是这种方法笔者反复尝试都没能调通,报告的错误是缺少Resource协议,估计第二种方法用到J2ME的某些扩展类包,此处不再深究。由于以前已经做过一些类似华容道这样的地图,这里直接给出Map类的代码,后面就不再详细解释Map类了,以便于我们可以集中精力处理Displayable1中的逻辑。Map类的代码如下:
package huarongroad;
import java.io.InputStream; import javax.microedition.lcdui.*;
public class Map { //处理游戏的地图,负责从外部文件加载地图数据,存放地图数据,并按照地图数据绘制地图
public byte Grid[][];//存放地图数据
public Map() {//构造函数,负责初始化地图数据的存储结构 this.Grid = new byte[Images.HEIGHT][Images.WIDTH]; //用二维数组存放地图数据,注意第一维是竖直坐标,第二维是水平坐标 }
public int[] read_map(int i) { <A href="file://从">file://从</A>外部文件加载地图数据,并存放在存储结构中,返回值是光标点的位置 //参数是加载地图文件的等级 int[] a = new int[2];//光标点的位置,0是水平位置,1是竖直位置 try { InputStream is = getClass().getResourceAsStream( "/huarongroad/level".concat(String.valueOf(i))); if (is != null) { for (int k = 0; k < Images.HEIGHT; k++) { for (int j = 0; j < Images.WIDTH; j++) { this.Grid[k][j] = (byte) is.read(); if ( this.Grid[k][j] == Images.CURSOR ) { //判断出光标所在位置 a[0] = j;//光标水平位置 a[1] = k;//光标竖直位置 this.Grid[k][j] = Images.BLANK;//将光标位置设成空白背景 } } is.read();//读取回车(13),忽略掉 is.read();//读取换行(10),忽略掉 } is.close(); }else { //读取文件失败 a[0] = -1; a[1] = -1; } }catch (Exception ex) { //打开文件失败 a[0] = -1; a[1] = -1; } return a; }
public boolean draw_map(Graphics g) { //调用Draw类的静态方法,绘制地图 try { for (int i = 0; i < Images.HEIGHT; i++) { for (int j = 0; j < Images.WIDTH; j++) { Draw.paint(g, this.Grid[i][j], j, i);//绘制地图 } } return true; }catch (Exception ex) { return false; } } } |
对于像华容道这样的小型地图可以直接用手工来绘制地图的内容,比如:
fa1c
2232
bd1e
2gg2
gihg
但是,如果遇到像坦克大战或超级玛莉那样的地图,就必须另外开发一个地图编辑器了(我会在后续的文章中介绍用vb来开发一个地图编辑器)。
五、详细设计
详细设计是程序开发过程中至关重要的一个环节,好在我们在前面的各个阶段中已经搭建好了项目所需的一些工具,现在这个阶段中我们只需集中精力设计好Displayable1中的逻辑。(两天的时间当然不只干这点活,还要把其他几个类的设计修改一下)
Displayable1这个类负责处理程序的控制逻辑。首先,它需要有表示当前关面的变量level、表示当前光标位置的变量loc、表示要移动区域的变量SelectArea、表示要移动到的区域的变量MoveArea、表示是否已有区域被选中而准备移动的变量Selected和Map类的实例MyMap。然后,我们根据用户按不同的键来处理不同的消息,我们要实现keyPressed()函数,在函数中我们处理按键的上下左右和选中(Fire),这里的处理需要我展开来讲一讲,后面我很快会把这一部分详细展开。
接下来,是实现paint()函数,我们打算在这一部分中反复的重画背景、地图和选择区域,这个函数必须处理好区域被选中之后的画笔颜色的切换,具体讲就是在没有选中任何区域时要用黑色画笔,当选重要移动的区域时使用绿色画笔,当选择要移动到的区域时改用红色画笔(当然附加一张流程图是必不可少的)。
再下面要实现的setRange()函数和setMoveRange()函数,这两个函数用来设置要移动的区域和要移动到的区域,我的思路就是利用前面在Images类中介绍过的地图组合标记常量,当移动到地图组合标记常量时,根据该点地图中的值做逆向变换找到相应的地图标记常量,然后设置相应的loc、SelectArea和MoveArea,其中setMoveRange()函数还用到了一个辅助函数isInRange(),isInRange()函数是用来判断给定的点是否在已选中的要移动的区域之内,如果isInRange()的返回值是假并且该点处的值不是空白就表明要移动到的区域侵犯了其他以被占用的区域。有了setRange()和setMoveRange()函数,Move()函数就水到渠成了,Move()函数将要移动的区域移动到要移动到的区域,在移动过程中分为三步进行:
第一.复制要移动的区域;
第二.将复制出的要移动区域复制到要移动到的区域(这两步分开进行的目的是防止在复制过程中覆盖掉要移动的区域);
第三.用isInRange2()判断给定的点是否在要移动到的区域内,将不在要移动到的区域内的点设置成空白。
下面我们详细的分析一下keyPressed()函数的实现方法:首先,keyPressed()函数要处理按键的上下左右和选中(Fire),在处理时需要用Canvas类的getGameAction函数来将按键的键值转换成游戏的方向,这样可以提高游戏的兼容性(因为不同的J2ME实现,其方向键的键值不一定是相同的)。
接下来,分别处理四个方向和选中.当按下向上时,先判断是否已经选定了要移动的区域(即this.selected是否为真),如果没有选中要移动区域则让光标向上移动一格,然后调用setRange()函数设置选择要移动的区域,再调用repaint()函数刷新屏幕,否则如果已经选中了要移动的区域,就让光标向上移动一格,然后调用setMoveRange()函数判断是否能够向上移动已选中的区域,如果能移动就调用repaint()函数刷新屏幕,如果不能移动就让光标向下退回到原来的位置。
当按下向下时,先判断是否已经选定了要移动的区域,如果没有选中要移动的区域则判断当前所处的区域是否为两个格高,如果是两个格高则向下移动两格,如果是一个格高则向下移动一格,接着再调用setRange()函数设置选择要移动的区域,而后调用repaint()函数刷新屏幕,否则如果已经选中了要移动的区域,就让光标向下移动一格,然后调用setMoveRange()函数判断是否能够向下移动已选中的区域,如果能移动就调用repaint()函数刷新屏幕,如果不能移动就让光标向上退回到原来的位置.按下向左时情况完全类似向上的情况,按下向右时情况完全类似向下的情况,因此这里不再赘述,详细情况请参见程序的源代码。
当按下选中键时,先判断是否已经选中了要移动的区域,如果已经选中了要移动的区域就调用Move()函数完成由要移动的区域到要移动到的区域的移动过程,接着调用repaint()函数刷新屏幕,然后将已选择标记置成false,继续调用win()函数判断是否完成了任务,否则如果还没有选定要移动的区域则再判断当前选中区域是否为空白,如果不是空白就将选中标记置成true,然后刷新屏幕.这里介绍一个技巧,在开发程序遇到复杂的逻辑的时候,可以构造一格打印函数来将所关心的数据结构打印出来以利调试,这里我们就构造一个PrintGrid()函数,这个函数纯粹是为了调试之用,效果这得不错.至此我们完成了编码前的全部工作。
六.编码
整个项目共有五个类,有四个类的代码前面已经介绍过了,而且是在其他项目中使用过的相对成熟的代码.现在只需全力去实现Displayable1类.Displayable1类的代码如下:
package huarongroad;
import javax.microedition.lcdui.*;
public class Displayable1 extends Canvas implements CommandListener {
private int[] loc = new int[2]; <A href="file://光">file://光</A>标的当前位置,0是水平位置,1是竖直位置 private int[] SelectArea = new int[4];//被选定的区域,即要移动的区域 private int[] MoveArea = new int[4];//要移动到的区域 private Map MyMap = new Map();//地图类 private boolean selected;//是否已经选中要移动区域的标志 private int level;//但前的关面 public Displayable1() {//构造函数 try { jbInit();//JBuilder定义的初始化函数 }catch (Exception e) { e.printStackTrace(); } } private void Init_game(){ //初始化游戏,读取地图,设置选择区域,清空要移动到的区域 this.loc = MyMap.read_map(this.level);//读取地图文件,并返回光标的初始位置 //0为水平位置,1为竖直位置 this.SelectArea[0] = this.loc[0];//初始化选中的区域 this.SelectArea[1] = this.loc[1]; this.SelectArea[2] = 1; this.SelectArea[3] = 1; this.MoveArea[0] = -1;//初始化要移动到的区域 this.MoveArea[1] = -1; this.MoveArea[2] = 0; this.MoveArea[3] = 0; } private void jbInit() throws Exception {//JBuilder定义的初始化函数 <A href="file://初">file://初</A>始化实例变量 this.selected = false;//设置没有被选中的要移动区域 this.level = 1; Images.init();//初始化图片常量 Init_game();//初始化游戏,读取地图,设置选择区域,清空要移动到的区域 setCommandListener(this);//添加命令监听,这是Displayable的实例方法 addCommand(new Command("Exit", Command.EXIT, 1));//添加“退出”按钮 }
public void commandAction(Command command, Displayable displayable) { //命令处理函数 if (command.getCommandType() == Command.EXIT) {//处理“退出” MIDlet1.quitApp(); } }
protected void paint(Graphics g) { //画图函数,用于绘制用户画面,即显示图片,勾画选中区域和要移动到的区域 try { g.drawImage(Images.image_Frame, 0, 0, Graphics.TOP | Graphics.LEFT);//画背景 MyMap.draw_map(g);//按照地图内容画图 if ( this.selected ) g.setColor(0,255,0);//如果被选中,改用绿色画出被选中的区域 g.drawRect(this.SelectArea[0] * Images.UNIT + Images.LEFT, this.SelectArea[1] * Images.UNIT + Images.TOP, this.SelectArea[2] * Images.UNIT, this.SelectArea[3] * Images.UNIT);//画出选择区域, <A href="file://如">file://如</A>果被选中,就用绿色 <A href="file://否">file://否</A>则,使用黑色 g.setColor(255,255,255);//恢复画笔颜色 if (this.selected) {//已经选中了要移动的区域 g.setColor(255, 0, 255);//改用红色 g.drawRect(this.MoveArea[0] * Images.UNIT + Images.LEFT, this.MoveArea[1] * Images.UNIT + Images.TOP, this.MoveArea[2] * Images.UNIT, this.MoveArea[3] * Images.UNIT);//画出要移动到的区域 g.setColor(255, 255, 255);//恢复画笔颜色 } }catch (Exception ex) { } System.out.println(Runtime.getRuntime().freeMemory()); System.out.println(Runtime.getRuntime().totalMemory()); }
private void setRange() { //设置移动后能够选中的区域 //调整当前光标位置到地图的主位置,即记录人物信息的位置 if (this.MyMap.Grid[this.loc[1]][this.loc[0]] == Images.DLEFT) { this.loc[0] -= 1;//向左调 }else if (this.MyMap.Grid[this.loc[1]][this.loc[0]] == Images.DUP) { this.loc[1] -= 1;//向上调 }else if (this.MyMap.Grid[this.loc[1]][this.loc[0]] == Images.DLEFTUP) { this.loc[0] -= 1;//向左调 this.loc[1] -= 1;//向上调 } this.SelectArea[0] = this.loc[0];//设置光标的水平位置 this.SelectArea[1] = this.loc[1];//设置光标的竖直位置 //设置光标的宽度 if (this.loc[0] + 1 < Images.WIDTH) { this.SelectArea[2] = this.MyMap.Grid[this.loc[1]][this.loc[0] + 1] != (byte) '1' ? 1 : 2; }else { this.SelectArea[2] = 1; } //设置光标的高度 if (this.loc[1] + 1 < Images.HEIGHT) { this.SelectArea[3] = this.MyMap.Grid[this.loc[1] + 1][this.loc[0]] != (byte) '2' ? 1 : 2; }else { this.SelectArea[3] = 1; } }
private boolean setMoveRange() { //设置要移动到的区域,能够移动返回true,否则返回false for (int i = 0; i < this.SelectArea[2]; i++) { for (int j = 0; j < this.SelectArea[3]; j++) { if (this.loc[1] + j >= Images.HEIGHT || this.loc[0] + i >= Images.WIDTH || (!isInRange(this.loc[0] + i, this.loc[1] + j) && this.MyMap.Grid[this.loc[1] + j][this.loc[0] + i] != Images.BLANK)) { return false; } } } this.MoveArea[0] = this.loc[0]; this.MoveArea[1] = this.loc[1]; this.MoveArea[2] = this.SelectArea[2]; this.MoveArea[3] = this.SelectArea[3]; return true; }
private boolean isInRange(int x, int y) { //判断给定的(x,y)点是否在选定区域之内,x是水平坐标,y是竖直坐标 if (x >= this.SelectArea[0] && x < this.SelectArea[0] + this.SelectArea[2] && y >= this.SelectArea[1] && y < this.SelectArea[1] + this.SelectArea[3]) { return true; }else { return false; } }
private boolean isInRange2(int x, int y) { //判断给定的(x,y)点是否在要移动到的区域之内,x是水平坐标,y是竖直坐标 if (x >= this.MoveArea[0] && x < this.MoveArea[0] + this.MoveArea[2] && y >= this.MoveArea[1] && y < this.MoveArea[1] + this.MoveArea[3]) { return true; }else { return false; } }
protected void keyPressed(int keyCode) { //处理按下键盘的事件,这是Canvas的实例方法 switch (getGameAction(keyCode)) {//将按键的值转化成方向常量 case Canvas.UP://向上 if (!this.selected) {//还没有选定要移动的区域 if (this.loc[1] - 1 >= 0) {//向上还有移动空间 this.loc[1]--;//向上移动一下 setRange();//设置光标移动的区域,该函数能将光标移动到地图主位置 repaint();//重新绘图 } }else {//已经选定了要移动的区域 if (this.loc[1] - 1 >= 0) {//向上还有移动空间 this.loc[1]--;//向上移动一下 if (setMoveRange()) {//能够移动,该函数能够设置要移动到的区域 repaint();//重新绘图 }else {//不能移动 this.loc[1]++;//退回来 } } } break; case Canvas.DOWN://向下 if (!this.selected) {//还没有选定要移动的区域 if (this.loc[1] + 1 < Images.HEIGHT) {//向下还有移动空间 if (this.MyMap.Grid[this.loc[1] + 1][this.loc[0]] == Images.DUP){//该图片有两个格高 this.loc[1]++;//向下移动一下 if (this.loc[1] + 1 < Images.HEIGHT) {//向下还有 <A href="file://移">file://移</A>动空间 this.loc[1]++;//向下移动一下 setRange();//设置光标移动的区域, <A href="file://该">file://该</A>函数能将光标移动到地图主位置 repaint();//重新绘图 }else {//向下没有移动空间 this.loc[1]--;//退回来 } }else {//该图片只有一个格高 this.loc[1]++;//向下移动一下 setRange();//设置光标移动的区域, <A href="file://该">file://该</A>函数能将光标移动到地图主位置 repaint();//重新绘图 } }else { } }else {//已经选定了要移动的区域 if (this.loc[1] + 1 < Images.HEIGHT) {//向下还有移动空间 this.loc[1]++;//向下移动一下 if (setMoveRange()) {//能够移动,该函数能够设置要移动到的区域 repaint();//重新绘图 }else {//不能移动 this.loc[1]--;//退回来 } } } break; case Canvas.LEFT://向左 if (!this.selected) {//还没有选定要移动的区域 if (this.loc[0] - 1 >= 0) {//向左还有移动空间 this.loc[0]--;//向左移动一下 setRange();//设置光标移动的区域,该函数能将光标移动到地图主位置 repaint();//重新绘图 } }else {//已经选定了要移动的区域 if (this.loc[0] - 1 >= 0) {//向左还有移动空间 this.loc[0]--;//向左移动一下 if (setMoveRange()) {//能够移动,该函数能够设置要移动到的区域 repaint();//重新绘图 }else {//不能移动 this.loc[0]++;//退回来 } } } break; case Canvas.RIGHT://向右 if (!this.selected) {//还没有选定要移动的区域 if (this.loc[0] + 1 < Images.WIDTH) {//向右还有移动空间 if (this.MyMap.Grid[this.loc[1]][this.loc[0] + 1] == Images.DLEFT) {//该图片有两个格宽 this.loc[0]++;//向右移动一下 if (this.loc[0] + 1 < Images.WIDTH) {//向右还有 <A href="file://移">file://移</A>动空间 this.loc[0]++;//向右移动一下 setRange();//设置光标移动的区域, <A href="file://该">file://该</A>函数能将光标移动到地图主位置 repaint();//重新绘图 }else {//向右没有移动空间 this.loc[0]--;//退回来 } }else {//该图片只有一个格宽 this.loc[0]++;//向右移动一下 setRange();//设置光标移动的区域, <A href="file://该">file://该</A>函数能将光标移动到地图主位置 repaint();//重新绘图 } }else { } }else {//已经选定了要移动的区域 if (this.loc[0] + 1 < Images.WIDTH) {//向右还有移动空间 this.loc[0]++;//向右移动一下 if (setMoveRange()) {//能够移动,该函数能够设置要移动到的区域 repaint();//重新绘图 }else {//不能移动 this.loc[0]--;//退回来 } } } break; case Canvas.FIRE: if (this.selected) {//已经选定了要移动的区域 Move();//将要移动的区域移动到刚选中的区域 repaint();//重新绘图 this.selected = false;//清除已选定要移动区域的标志 if ( win()) { System.out.println("win"); } }else {//还没有选定要移动的区域 if (this.MyMap.Grid[this.loc[1]][this.loc[0]] == Images.BLANK) {//要移到的位置是一个空白 }else {//要移到的位置不是空白 this.selected = true;//设置已选定要移动区域的标志 } repaint();//重新绘图 } break; } }
private boolean win(){ <A href="file://判">file://判</A>断是否已经救出了曹操 if ( this.MyMap.Grid[Images.HEIGHT - 2 ][Images.WIDTH - 3 ] == Images.CAOCAO ) return true; else return false; }
private void PrintGrid(String a) { <A href="file://打">file://打</A>印当前地图的内容,用于调试 System.out.println(a); for (int i = 0; i < Images.HEIGHT; i++) { for (int j = 0; j < Images.WIDTH; j++) { System.out.print( (char)this.MyMap.Grid[i][j]); } System.out.println(""); } }
private void Move() { <A href="file://将">file://将</A>要移动的区域移动到刚选中的区域 if (this.MoveArea[0] == -1 || this.MoveArea[1] == -1 || this.SelectArea[0] == -1 || this.SelectArea[1] == -1) {//没有选中区域 }else {//已经选中了要移动的区域和要移动到的区域 byte[][] temp = new byte[this.SelectArea[3]][this.SelectArea[2]]; <A href="file://复">file://复</A>制要移动的区域,因为这块区域可能会被覆盖掉 for (int i = 0; i < this.SelectArea[2]; i++) { for (int j = 0; j < this.SelectArea[3]; j++) { temp[j][i] = this.MyMap.Grid[this.SelectArea[1] +j] [this.SelectArea[0] + i]; } } <A href="file://PrintGrid">file://PrintGrid</A>("1"); // 调试信息 <A href="file://将">file://将</A>要移动的区域移动到刚选中的区域(即要移动到的区域) for (int i = 0; i < this.SelectArea[2]; i++) { for (int j = 0; j < this.SelectArea[3]; j++) { this.MyMap.Grid[this.MoveArea[1] + j] [this.MoveArea[0] + i] = temp[j][i]; } } <A href="file://PrintGrid">file://PrintGrid</A>("2");// 调试信息 <A href="file://将">file://将</A>要移动的区域中无用内容置成空白 for (int i = 0; i < this.SelectArea[3]; i++) { for (int j = 0; j < this.SelectArea[2]; j++) { if (!isInRange2(this.SelectArea[0] + j, this.SelectArea[1] + i)) {//该点是不在要移动到 <A href="file://的">file://的</A>区域之内,需置空 this.MyMap.Grid[this.SelectArea[1] + i] [this.SelectArea[0] + j] = Images.BLANK; }else { } } } <A href="file://PrintGrid">file://PrintGrid</A>("3");// 调试信息 this.SelectArea[0] = this.MoveArea[0];//重置选中位置的水平坐标 this.SelectArea[1] = this.MoveArea[1];//重置选中位置的竖直坐标 this.MoveArea[0] = -1;//清空要移动到的位置 this.MoveArea[1] = -1;//清空要移动到的位置 this.MoveArea[2] = 0;//清空要移动到的位置 this.MoveArea[3] = 0;//清空要移动到的位置 } } } |
代码的相关分析,在详细设计阶段已经讲过,代码中有比较相近的注释,请读者自行研读分析.将全部的代码写好,用wtk2.0自带的Ktoolbar工具建立一个工程,接下来把去不源文件放到正确位置下,然后点击build,再点run,就完成了程序的编写.当然如果有错误还要修改和调试.
七、测试
作为一个真正的产品要经过单体测试、结合测试和系统测试。由于项目本身简单,而且大部分代码已经是相对成熟的,我们跳过单体测试;又由于笔者的实际环境所限,无法搞到Java手机,无法架设OTA服务器,因此我们也只能放弃系统测试。那么就让我们开始结合测试吧。测试之前要先出一个测试式样书,也就是测试的计划。我们将它简化一下,只测试如下几种情况:第一、对各种形状的区域的选择和移动;第二、临近边界区域的选择和移动;第三、同一区域的反复选择和反复移动;第四、非法选择和非法移动。有了测试的目标,接下来的工作就是用wtk2.0自带的Run MIDP Application工具进行测试。打开这个工具,加载huarongRoad的jad文件,程序就会自动运行,选择launch上MIDlet1这个程序,华容道游戏就会跃然屏幕之上,接下来的工作就是左三点.右三点,拇指扭扭,来做测试。测试过程中发现任何的问题,立刻发一个bug票给自己,然后就又是痛苦的调试和修正bug,如此如此。
八.发布
谈到发布,其实是个关键,再好的产品不能很好的发布出去也只是个产品而已,变不成商品也就得不到回报.由于笔者的条件所限,这里只能是纸上谈兵,不过还是希望能够使读者对这一过程有所了解(网上的资料也很多)。
J2ME的程序发布一般都是通过OTA(Over The Air),你只需要一台有公网IP的主机和一个普通的web Server就可以了(尽管要求很低,但笔者还是没有),这里我们以apache为例介绍一下OTA服务的配置,首先是安装好了apache服务器,然后在conf目录下找到mime.types文件,在该文件中加入如下两行
application/java-archive jar
text/vnd.sun.j2me.app-descriptor jad
然后重起apache服务器就可以了。接下来的工作就是修改jad文件中MIDlet-Jar-URL:后面的参数,将它改为URL的绝对路径,即<A href="http://***/">http://***/</A>huarongroad.jar(其中***是你的域名或IP地址)。在下面就是用java手机下载jad文件,它会自动部署相应的jar文件并加载它。剩下的工作就和在模拟器上操作是一样的了。
九、项目总结
至此,我们已经完成了一个J2ME游戏的全部开发过程,程序中涉及到了调研、分析、设计、编码、测试和发布等方面的问题,其实在实际的工作中还有很多更为具体的问题,毕竟技术只在软件开发过程中占据很有限的一部分,这里限于篇幅的限制无法一一具体展开。今后,笔者计划再写一篇使用J2ME开发手机屏保的文章,借此机会向读者展示J2ME动画技术;然后再写一篇J2ME网络应用的文章,做一个类似开心辞典那样的知识问答游戏,以便向读者展示J2ME的网络技术;待这两方面的技术交待清楚之后,我将引领读者制作一个稍大一些的游戏。
韩国系MMORPG和EverQuest在怪物智能上的简单对比
现在,让我们假设诸位正在设计一款MMO ARPG中的怪物行动模式。
在主流的韩国系MMO ARPG中,这些问题一般被如下处理:
1. 敌人是否会主动攻击玩家人物?如何判断?
分为主动攻击和被动攻击两种类型。前者当玩家人物进入自身周围一定范围之后激发攻击行为。后者当自身被攻击后激发攻击行为。
2. 当怪物面对多个玩家的时候,如何选择自己的攻击对象?
主流的处理方式是攻击第一个攻击自己的玩家。
3. 战斗过程中,怪物能够改变自己的攻击目标吗?如何判断?
基本上不改变。
4. 当处于不利的情况下,怪物会作出什么行动?
死战到底,有些游戏中有逃跑的设定。
5. 怪物之间能否协作?
一拥而上。
在欧美经典MMORPG EverQuest中,是这样处理的:
1. 敌人是否会主动攻击玩家人物?如何判断?
通过一个关系表来决定,如果玩家和自身阵营关系恶劣,那么主动攻击。否则是被动攻击。(这个关系表是动态的,会根据玩家的行为而改变)
2. 当怪物面对多个玩家的时候,如何选择自己的攻击对象?
攻击首先进入自己视野或者攻击自己的玩家。
3. 战斗过程中,怪物能够改变自己的攻击目标吗?如何判断?
战斗中,怪物有一个隐藏的仇恨列表。怪物会攻击处于自己仇恨列表顶端的玩家,而玩家会增加仇恨的动作包括攻击、挑衅、医疗同伴、施放魔法……等等。换言之,如果一个法师疯狂的施放攻击法术,那么一定会被怪物首先料理。同样的,如果牧师频繁的医疗同伴,也会被怪物特殊照顾。(正确的战斗方式是让肉盾类角色保证自己永远处于仇恨列表最顶端,当然这很困难,但是却很有必要,也很有趣)
4. 当处于不利的情况下,怪物会作出什么行动?
逃跑,而且速度相当快。鉴于怪物之间能够呼唤,所以让怪物成功跑掉往往比较可怕。有一些法师系的敌人会用施放传送门的方式逃跑。
5. 怪物之间能否协作?
怪物之间的仇恨可以传递。如果两个怪物距离很近,那么很可能一个发现到了你而两个都冲过来。法师系的怪物也会给同伴加血和一些防御性法术。
简单的说,EQ在“人工智能”的范畴上并没有走多远,但是在游戏上却做的很出色。特别是“仇恨列表”,并不复杂的规则衍生出了很多的变化,堪称天才的设计。当你体会到因为这个限制而衍生出的精彩刺激的游戏过程,便能理解到玩家想要的其实并不仅仅是经验值而已。
试试看将“仇恨列表”导入你的游戏,你觉得会发生什么?