说到 SLG 游戏开发,无论其如何运转,里面都离不开各种数据的处理,一般来说,游戏越专业,需要处理的数据量将相对越大,类别也分得越细。
游戏离不开美工,也离不开策划,各项参数的专业划分同样是评价一款 SLG 游戏是否优秀的必要指标之一。所谓的好游戏仅仅画面出彩,配乐一流是绝对不够的,做“靓”很容易,做“专”则很难。
比如日本的超级机器人大战系列,自 90 年代初开始出现以来,截止到今天为止其中涉及的动漫超过 60 部,出场知名人物多达 600 名以上,几乎涵盖了日本所有知名机器人动画的机体(当然也有遗憾,比如机动警察)。这些角色无分善恶是各有 各 的 fans ,各有各的崇拜者。请这些“天王”出场,如果还做不好,过强或者过弱某一方,或者该出现的“华丽”技能没有出现,日本这些“ACG宅”的愤怒可是很恐怖的,正所谓“秋叶原前一声吼,东京也要抖三抖”。
对动漫人物的把握程度,对角色参数的设定,起了尤为重要的作用。
在这里鄙人不由得想起某位大神,就是 SRC (Simulation RPG Construction)的作者鬼子 Kei 。这家伙从 90 年代末就用 VB5.0 开始制作 SRC 这个机战的同人制作工具,而我这辈子读的第一段程序代码,也正是某杂志随盘附录的 SRC0.6 版及其源码,当时我连 VB 是什么都不知道,彻底的读天书,于是才买书钻研 VB ……一晃 10 年, VB6.0 我都已放下很久,他居然还在更新 SRC ,而且还是使用 VB5 开发,我不由惊叹鬼子的勤奋还有专注。 10 年工夫,我是无论如何也不信 Kei 不会用更简便的工具来制作 SRC 的,但是他却没有,硬是把 VB5 这个现在很多人用都没用过的古董级工具(实际上我也没用过 ||| )做出一款亚洲知名的机战同人开发工具来, 10 年来此人网站流量累计超过 1690 万,而且我也真的见过很多同人爱好者的 SLG 游戏是采用 SRC 开发。日本人真是恐怖,居然有人能甘心钻研 VB5 如此之久,如果把这种劲头用在工作上,想想我都不寒而栗,有这样的恒心这样弃而不舍的精神,在亚洲中国最大的潜在对手始终非日本莫属……咳咳,扯远了。
SRC运行画面,运行需要VB5运行库,并日文Windows环境。(或者先用AppLocale转内码,再转日文脚本乱码后载入)
通常来讲,我们不太可能将各种游戏数据硬编码到程序中,这样既不利于测试,也不方便重用,总需要一个外部文件作为存储介质。这时的选择其实很多,在 Java 游戏开发中我们即可以使用 xml 这类现有的规范格式,也可以干脆如 SRC 般自己定义脚本,或者将少量数据利用 properties 存储。
就我个人认为,自己订制游戏脚本格式从长远看是最可取的,以后的同类项目方便重用,也不容易被他人盗取数据。而 xml 虽然有很多现成的组件可用,但是处理复杂业务时就没有自己的脚本用着方便,而且当数据很少时也有些杀鸡用牛刀的感觉。至于 properties ,存取单键值的数据固然很方便,但是对于表格类的数据,即使很简单也不适用,至少不直观了。
在本例中我所采用的,是一种更为偷懒的方式,并不归属于以上所说,而是另辟蹊径的利用 csv 格式,实现了一种较为另类的表格式数据存储。
CSV ( Comma Separated value ),也叫逗号分隔值文件,是一种用来存储数据的纯文本文件格式,通常用于电子表格或数据库软件。我们打开 windows 记事本,随便打几个字母用“ , ”分割,再用 excel 查看,这时 excel 就会自动以表格方式显示这些数据。
同样对于 Java 中的表格数据存储,也可以采用了这种方式保存,并且利用 reflect 机制映射到类,看上去即直观,也比 xml 省心,比自己写脚本省力。
核心代码如下:
package org.loon.simple.slg.utils;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
public class CSVConfigure {
final static public Object[] loadPropertys( final String fileName,
final Class clazz) {
Object[] results = null ;
try {
List properts = CSVConfigure.loadPropertys(fileName);
int size = properts.size();
results = (Object[]) Array.newInstance(clazz, size);
for ( int i = 0 ; i < size; i++) {
Map property = (Map) properts.get(i);
Set set = property.entrySet();
results[i] = clazz.newInstance();
for (Iterator it = set.iterator(); it.hasNext();) {
Entry entry = (Entry) it.next();
ClassUtils.beanRegister(results[i],
(String) entry.getKey(), (String) entry.getValue());
}
}
} catch (Exception ex) {
throw new RuntimeException(ex+ " " +fileName);
}
return results;
}
final static public List loadPropertys( final String fileName) {
List result = new ArrayList();
try {
CSVReader csv = new CSVReader(fileName);
List names = csv.readLineAsList();
int length = names.size();
for (; csv.ready();) {
Map propertys = new HashMap(length);
String[] csvItem = csv.readLineAsArray();
for ( int i = 0 ; i < length; i++) {
propertys.put((String) names.get(i), csvItem[i]);
}
result.add(propertys);
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
}
使用方法:
friends = (Friend[]) CSVConfigure.loadPropertys(FRIEND_FILE_NAME,
Friend. class );
只是一个很简单的反射,就可以将CSV表格数据注射到类上。
再说一下游戏数据的存储,一般来讲就是指游戏记录,这在 Java 中是最好办的。因为只要你的关键数据对象实现 serializable ,大可以直接将当前状态序列化到本地文件中,不过是 ObjectInputStream 和 ObjectOutputStream 的把戏罢了。
如果没有或者你不想的话,你只要将关键数据以某种格式保存(这个真是随便,能再读出来就成),然后再反馈给游戏即可。实际上我们都知道所谓读档 / 保存并不是时间真的重来了,而是数据被还原罢了。
用例代码:
package org.loon.simple.slg.utils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public class Keep {
private static final long serialVersionUID = -1982090153295778606L;
final static public String LS = System.getProperty( "line.separator" , "/n" );
final static public String FS = System.getProperty( "file.separator" , "//" );
final static public String addChange(final String s, final int x) {
String result = null ;
StringBuilder sbr = new StringBuilder();
for ( int i = 0; i < s.length(); i++) {
char p = ( char ) (s.charAt(i) + x);
sbr.append(p);
}
try {
result = URLEncoder.encode(sbr.toString(), "UTF-8" );
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return result;
}
final static public String backChange(final String s, final int x) {
String result = null ;
try {
result = URLDecoder.decode(s, "UTF-8" );
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
StringBuilder sbr = new StringBuilder();
for ( int i = 0; i < result.length(); i++) {
char p = ( char ) (result.charAt(i) - x);
sbr.append(p);
}
return sbr.toString();
}
public static byte [] compress(final byte [] buffer) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzipos = new GZIPOutputStream(baos);
gzipos.write(buffer);
gzipos.flush();
gzipos.close();
return baos.toByteArray();
} catch (IOException e) {
return null ;
}
}
public static byte [] uncompress(final byte [] buffer) {
try {
GZIPInputStream gzipis = new GZIPInputStream(
new ByteArrayInputStream(buffer));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte [] tmp = new byte [8192];
int len;
while ((len = gzipis.read(tmp)) > 0) {
baos.write(tmp, 0, len);
}
baos.flush();
return baos.toByteArray();
} catch (IOException e) {
return null ;
}
}
public static void save(final String fileName,final String message) throws IOException {
save( new File(fileName), new ByteArrayInputStream(Keep.compress(message.getBytes())));
}
private static void save(final File file,final InputStream input)
throws IOException {
mkdirs(file);
BufferedOutputStream output = null ;
try {
int contentLength = input.available();
output = new BufferedOutputStream(
new FileOutputStream(file, false ));
while (contentLength-- > 0) {
output.write(input.read());
}
} finally {
close(input, file);
close(output, file);
}
}
final private static byte [] read(final InputStream inputStream) {
byte [] arrayByte = null ;
BufferedInputStream buffer = new BufferedInputStream(inputStream);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte [] bytes = new byte [8192];
try {
int read;
while ((read = buffer.read(bytes)) >= 0) {
byteArrayOutputStream.write(bytes, 0, read);
}
arrayByte = byteArrayOutputStream.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
if (buffer != null ) {
buffer.close();
buffer = null ;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return Keep.uncompress(arrayByte);
}
public static List load(final String fileName) throws IOException {
File file = new File(fileName);
BufferedReader reader = new BufferedReader( new InputStreamReader( new ByteArrayInputStream(Keep.read( new FileInputStream(file)))));
List records = new ArrayList();
String record = null ;
try {
while ((record = reader.readLine()) != null ) {
records.add(record);
}
} finally {
close(reader, file);
}
return records;
}
private static void mkdirs(final File file) throws IOException {
checkFile(file);
File parentFile = file.getParentFile();
if (parentFile != null ) {
if (!parentFile.exists() && !parentFile.mkdirs()) {
throw new IOException( "Creating directories "
+ parentFile.getPath() + " failed." );
}
}
}
private static void checkFile(final File file) throws IOException {
if (file.exists() && !file.isFile()) {
throw new IOException( "File " + file.getPath()
+ " is actually not a file." );
}
}
private static void close(final InputStream input,final File file) {
if (input != null ) {
try {
input.close();
} catch (IOException e) {
closingFailed(file, e);
}
}
}
private static void close(final OutputStream output,final File file) {
if (output != null ) {
try {
output.close();
} catch (IOException e) {
closingFailed(file, e);
}
}
}
private static void close(final Reader reader,final File file) {
if (reader != null ) {
try {
reader.close();
} catch (IOException e) {
closingFailed(file, e);
}
}
}
private static void closingFailed(final File file,final IOException e) {
String message = "Closing file " + file.getPath() + " failed." ;
throw new RuntimeException(message + ":" + e.getMessage());
}
}
导出的结果:
比如在这个示例游戏中,我将保存的数据先递增字符位数,再经过 URL 编码,最后 gzip 压缩,出来的一组鬼画符般记录文档,唯一的作用就是不让人轻易读出并修改罢了。当然这只是最简单的例子,我们完全加密的更复杂(比如玩玩 DES ),验证的更变态,让玩家绞尽脑汁也无法修改游戏分毫,毕竟让玩家快乐且痛苦的游戏,就是游戏制作者最大的乐趣及兴奋点啊,哈哈哈。(惊见板砖 + 臭鸡蛋,我闪 ~~~ )
主菜单界面: 游戏基本界面,背景设定主角在一座城镇中,有五项基本命令可供选择。 队友雇用界面: 即酒店界面,用于寻找战友加入 物品购入界面: 商店,用于补充游戏中物品 物品装备界面: 道具装备,用于装备购入的物品 关卡选择界面: 任务选择,本示例由于没有考虑做大,所以直接将关卡做成赏金模式供玩家选择 战斗画面: 最初有过在此例复刻梦幻模拟战2的打算,但由于没有找到整套的素材,所以作罢。谁有兴趣帮兄弟找到整套素材(关键是各兵种战斗图)的话,我可以再作一个复刻梦幻2的例子。
SLG游戏入门示例及源码下载地址:http://download.csdn.net/source/809105