零打碎敲学Android(五)—AVG,只有神知道的世界

AVG,英文全称Adventure Game,缩写为AVG或ADV,是电子游戏中的一个大类就足够了。事实上,AVG的范畴相当庞杂,很难单纯用文字说清,关于AVG游戏,笔者此处不想解释太细,以前的博文中也已经说过很多次了。如果谁想要完整了解它的涵义,可以去追《只有神知道的世界》这部漫画,虽不中,亦不远也。

传统形式的冒险游戏黄金时期,几乎和80年代前半的8位元电脑风潮同时期。虽然此后让出了电子游戏主角的位置,不过在手机功能发达之后,亦有过去名作陆续移植的动作。就笔者来看,AVG游戏可以算得上最适合在Android上普及的游戏类型之一,这是AVG游戏的特性所决定的。

我们大家都知道,早期的计算机游戏基本上是通过文字来进行的,也就是所谓的文字游戏。玩家在电脑终端以文本的方式输入指令,系统也以文字的方式提供反馈。这种文字式的AVG(冒险游戏) 堪称现代RPG游戏的鼻祖,它的多人在线版本MUD(都还记得《网络创世纪》吧?),则直接演变成了如今的各种网络游戏。这种类型的游戏能够获得成功,其非线性的故事情节起到了关键性的作用。但随着技术的发展,欧美的游戏制作者们渐渐抛弃了文字AVG的平台,而把精力转入了如Doom、Quake 这样靠强大的图像引擎取胜的游戏开发。

所幸的是,文字AVG游戏并没有因此消亡,而是被精明的日本游戏制作人所采用,在突飞猛进的日本游戏市场获得了新生,并逐步发展成为当今著名的日式AVG流派,守住了一片天空。而由日式AVG游戏衍生出来的旁支:互动电子小说,则更是吸引了越来越多的Fans,在日本的游戏产业中占据了重要的地位。

所谓日式AVG,就是在最初的文字冒险游戏的基础上利用精美的CG图片和动人的音响效果加以强化,靠优秀的文字和剧情打动人心的一种游戏形式。比如《心跳回忆》这样的恋爱游戏,玩家通过指令控制主人公的行动,而效果则通过屏幕上的CG图片和剧情文字(主要是对话)来表示,这就是典型的日式AVG游戏。互动电子小说与传统的日式AVG相比较往往描写更细致,情节更动人,具有更高的内涵,且文字量大,引人入胜的剧情是其主要魅力。这种互动电子小说一般出自专业作家之手,文学性极强,题材涉及面广,爱情、侦探、恐怖等无所不包。

此类型游戏多采取玩家输入或选择指令以改变行动的形式进行,强调故事线索的发掘,主要考验玩家的观察力和分析能力。游戏有时候很像角色扮演游戏,但不同的是,冒险游戏中玩家操控的游戏主角本身的属性能力一般是固定不变并且不会影响游戏的进程。

“费时不费力,劳心不劳神”,AVG先天具备的这种游戏特性,决定了它与网络小说一样,生来就是吸引广大“闲人”参与的“手机杀手级”应用。打不通《波斯王子》的“小白”大有人在,打不通AVG的“小白”(不是指全结局通关|||),却几乎不存在于这个世界之上。

关于日式AVG与互动电子小说的开发,主要有三个方面的因素:游戏引擎、游戏剧本/游戏脚本编写、CG和音效制作。

一、游戏引擎

需要指出的是,日式AVG与互动电子小说的游戏引擎的基本结构通常是相当简单的。其原理是按一定格式读取制作好的故事脚本,再从资源文件中取出相应的文字信息或CG图片等在屏幕上展示出来。一个普通的程序员(不一定要专业搞游戏,例如偶这样的)完全有能力胜任这方面的开发工作。而且,现在市场上有很多公开发售甚至免费的AVG引擎(请参见笔者博文:同人游戏开发工具巡礼——AVG(ADV)引擎篇 ),都已经有了相当高的集成度。

二、游戏剧本/脚本编写

剧本方面可以原创,也可以拿现有的文学作品改编。剧本不一定要出自专业作家之手,但是,最好他能够具备一定的知名度或者受众群体,因为一个著名的作家,不但可以提供优质的剧情,还可以作为产品的一大卖点用来炒作。与传统小说不同的是,AVG游戏更强调的是一种非线性的故事结构,玩家选择不同的分支可以引入不同的结局,这一点是至关重要的,否则,游戏的乐趣将大打折扣。剧本完成之后,将由专门的录入人员以特定的格式转化为游戏脚本(Script),供游戏引擎使用。关于脚本格式,可以自行定制,也可以使用现成的脚本语言(比如Lua),没有强制的规定,但是同一系列游戏中应该尽量统一脚本语言。

三、CG和音效

事实上,这才是一部AVG游戏成功的关键之所在。历史上所有成功的日式AVG游戏无不具有非常优秀的美工和音效。比如《Fate》就因为包括了许多漂亮的CG图画和非常好听的人物配音,而一夜成名,从最初的同人H游戏,转为了正归的galgame。精美的图像和动听的音响,已经不再仅仅是对文字剧情的强化,而成为了整个故事的有机组成部分,是其区别于纸上读物的重要标志。我常和人说,想推游戏,那么“一策划、二美工、三程序”(呜呜呜呜~),指的就是这么一回事。

下图为2009年,以传说中的国产游戏引擎【古月引擎】开发的传说中的国产AVG游戏大作《红楼梦》(说实话,笔者更怀念当初智冠的《红楼梦之十二金钗》……)

00

01

说了半天,现在,我们自己也来写一款能够运行在Android上的AVG游戏引擎吧!

源码下载地址:http://code.google.com/p/loon-simple/downloads/list

Android版源码(PC版请见以前博文,不再赘述)

package org.loon.framework.android.game.avg; import java.util.List; import org.loon.framework.android.game.LAFont; import org.loon.framework.android.game.LAGraphics; import org.loon.framework.android.game.LAGraphicsUtils; import org.loon.framework.android.game.LAScreen; import org.loon.framework.android.game.LASystem; import org.loon.framework.android.game.LTimerContext; import org.loon.framework.android.game.command.Command; import android.graphics.Color; import android.view.KeyEvent; import android.view.MotionEvent; /** * * Copyright 2008 - 2009 * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * * @project loonframework * @author chenpeng * @email <a title="" href="http://hi.baidu.com/ceponline" mce_href="http://hi.baidu.com/ceponline" target="_blank">ceponline</a>@yahoo.com.cn * @version 0.1.0 */ public class AVGScript extends LAScreen { private int color = -1; private int sleep, sleepMax; private boolean isClick,isNext, isMessage, isSelectMessage,run; private String scriptName; private int stringMaxLine = 8, selectFlag = 0; private String roleName; private CG cg; private Command command; private MessagePrint mesPrint = new MessagePrint(0, 210); private String[] selectMessages; private int[] flags; public AVGScript(final String initscript) { format(initscript); } public void format(final String initscript) { initialize(initscript); runScript(scriptName); } private void initialize(final String initscript) { scriptName = initscript; sleep = 0; selectFlag = 0; isClick = false; isSelectMessage = false; isMessage = false; cg = new CG(); selectMessages = new String[stringMaxLine]; flags = new int[stringMaxLine]; } public void finalize() { flush(); } public void flush() { cg = null; mesPrint = null; selectMessages = null; flags = null; } public boolean nextMessages() { return mesPrint.next(); } /** * 绘制游戏界面 */ public void draw(LAGraphics g) { if (sleep == 0) { if (cg.getBackgroundCG() != null) { g.drawImage(cg.getBackgroundCG(), 0, 0); } int moveCount = 0; for (int i = 0; i < cg.getCharas().size(); i++) { Chara chara = (Chara) cg.getCharas().get(i); float value = 1.0f; if (chara.next()) { value = chara.getNextAlpha(); moveCount++; } g.setAlpha(value); chara.draw(g); g.setAlpha(1.0f); } if (isMessage && selectMessages != null && selectMessages.length > 0) { int size = 20; Message.dialog.showDialog(g); LAFont font = g.getFont(); g.setFont("黑体", 0, size); g.setAntiAlias(true); g.setColor(Color.WHITE); Message.dialog.showRoleName(g, roleName); if (isSelectMessage) { char[] meschars; int sizeWidth = -(size * 2); int left = Message.dialog.getMESSAGE_LINE_X() + 2; int top = Message.dialog.getMESSAGE_LINE_Y() + 20; for (int i = 0; i < stringMaxLine; i++) { meschars = selectMessages[i].toCharArray(); for (int j = 0; j < meschars.length; j++) { g .drawString(String.valueOf(meschars[j]), (size * j) + left - sizeWidth, top + i * 20); } if (flags[selectFlag] != -1) { Message.dialog.showDialog(g, selectFlag, size, LASystem.FONT_SIZE); } } } else { mesPrint.next(); mesPrint.draw(g); } g.setAntiAlias(false); g.setFont(font); } } else { sleep--; if (color != -1) { float alpha = (float) (sleepMax - sleep) / sleepMax; if (alpha < 0.7) { if (cg.getBackgroundCG() != null) { g.drawImage(cg.getBackgroundCG(), 0, 0); } g.setAlpha(alpha); g.setColor(color); g.fillRect(0, 0, getWidth(), LASystem.getSystemHandler() .getHeight()); g.setAlpha(1.0f); } } } } public void addCG(String fileName) { addCG(fileName, 0, 0); } public void addCG(String fileName, int x, int y) { cg.addImage(fileName, x, y); } public synchronized void select(int type) { if (command != null) { command.select(type); isSelectMessage = false; } } public synchronized String getSelect() { if (command != null) { return command.getSelect(); } return null; } private void resetFlag() { if (!isMessage) { return; } if (selectMessages != null) { int count = (getTouch().y - Message.dialog.getMESSAGE_LINE_Y()) / 25; if (count < 0) { count = 0; return; } if (count >= stringMaxLine) { count = 0; return; } if (flags[count] != -1) { isClick = true; } int maxSize = 0; for (; maxSize < selectMessages.length; maxSize++) { if (selectMessages[maxSize].length() == 0) { break; } } maxSize -= 1; if (maxSize > 0 && count > maxSize) { count = maxSize; } selectFlag = count; } } private synchronized void nextScript() { isMessage = false; isClick = false; int count = 0; for (int i = 0; i < stringMaxLine; i++) { selectMessages[i] = ""; flags[i] = -1; } if (command != null) { for (; command.next();) { // 返回本行命令执行结果 String result = command.doExecute(); if (result == null) { nextScript(); break; } // 分解命令 List commands = Command.splitToList(result, " "); int size = commands.size(); String cmdFlag = (String) commands.get(0); String mesFlag = null, orderFlag = null, lastFlag = null; if (size == 2) { mesFlag = (String) commands.get(1); } else if (size == 3) { mesFlag = (String) commands.get(1); orderFlag = (String) commands.get(2); } else if (size == 4) { mesFlag = (String) commands.get(1); orderFlag = (String) commands.get(2); lastFlag = (String) commands.get(3); } if (cmdFlag.equalsIgnoreCase("wait")) { isMessage = true; break; } if (cmdFlag.equalsIgnoreCase("mes")) { roleName = null; isMessage = true; roleName = Command.getNameTag(mesFlag, "{", "}"); String nMessage = null; if (roleName != null) { int nameLength = roleName.length() + 2; nMessage = mesFlag.substring(nameLength, mesFlag .length()); } else { nMessage = mesFlag; } mesPrint.setMessage(nMessage); flags[count] = -1; if (++count == stringMaxLine) { break; } break; } if (cmdFlag.equalsIgnoreCase("selects")) { isMessage = true; isSelectMessage = true; String[] selects = command.getReads(); for (int i = 0; i < selects.length; i++) { selectMessages[i] = selects[i]; flags[i] = i; } break; } if (cmdFlag.equalsIgnoreCase("fname")) { roleName = null; break; } if (cmdFlag.equalsIgnoreCase("cgwait")) { isMessage = false; break; } if (cmdFlag.equalsIgnoreCase("sleep")) { sleep = Integer.valueOf(mesFlag).intValue(); sleepMax = Integer.valueOf(mesFlag).intValue(); isMessage = false; break; } if (cmdFlag.equalsIgnoreCase("flash")) { String[] colors = mesFlag.split(","); if (color == -1 && colors != null && colors.length == 3) { color = Color.rgb( Integer.valueOf(colors[0]).intValue(), Integer .valueOf(colors[1]).intValue(), Integer .valueOf(colors[2]).intValue()); sleep = 20; sleepMax = sleep; isMessage = false; } else { color = -1; } break; } if (cmdFlag.equalsIgnoreCase("run")) { run = true; isMessage = false; continue; } if (cmdFlag.equalsIgnoreCase("gb")) { if (mesFlag == null) { return; } if (mesFlag.equalsIgnoreCase("none")) { cg.setBackgroundCG(null); } else { cg .setBackgroundCG(LAGraphicsUtils .loadLAImage(mesFlag)); } continue; } if (cmdFlag.equalsIgnoreCase("cg")) { if (mesFlag == null) { return; } // 删除 if (mesFlag.equalsIgnoreCase("del")) { if (orderFlag != null) { cg.removeImage(orderFlag); } else { cg.clear(); } } else if (lastFlag != null && "to".equalsIgnoreCase(orderFlag)) { Chara chara = cg.removeImage(mesFlag); if (chara != null) { int x = chara.getX(); int y = chara.getY(); chara = new Chara(lastFlag, 0, 0); chara.setMove(false); chara.setX(x); chara.setY(y); cg.addChara(lastFlag, chara); } } else { // 移动 int x = 0, y = 0; if (orderFlag != null) { x = Integer.parseInt(orderFlag); } if (size >= 4) { y = Integer.parseInt((String) commands.get(4)); } cg.addImage(mesFlag, x, y); } continue; } } } } private synchronized void runScript(final String fileName) { if (fileName == null) { return; } if (command == null) { command = new Command(fileName); // 刷新脚本缓存 Command.resetCache(); } else { command.formatCommand(fileName); } nextScript(); } public void alter(LTimerContext timer){ resetFlag(); } public boolean onKeyDown(int keyCode, KeyEvent e) { return false; } public boolean onKeyUp(int keyCode, KeyEvent e) { return false; } public boolean onTouchDown(MotionEvent e) { if (run) { setScreen(new Title()); return true; } if (!isSelectMessage && sleep <= 0) { if (!isMessage) { isMessage = true; } isNext = true; } else if (isMessage && isClick) { if (flags[selectFlag] != -1) { // 变更选择变量 select(flags[selectFlag]); isNext = true; isSelectMessage = false; } } if (isNext && !isSelectMessage) { // 逐行解释执行脚本 nextScript(); } return true; } public boolean onTouchMove(MotionEvent e) { resetFlag(); return false; } public boolean onTouchUp(MotionEvent e) { return false; } }

效果如下图:

00

01

02

源码下载地址:http://code.google.com/p/loon-simple/downloads/list

就AVG发展现状而言,日式冒险游戏主流以称作音声小说与视觉小说的类型为大宗。这些游戏多半不如过去般着重解谜成分,而是当成故事的表现形式来制作。此类作品希望玩家如同小说一般阅览显示在画面上的讯息,选择的指令直接反映在剧情分歧上。其中更出现像《暮蝉悲鸣时》一样废除指令选择,要求玩家推理事件、在网络上交换推理情报的作品。对于这个现状,有很多资历玩家抱持否定态度,但是,此类作品依旧相当受欢迎,比如《暮蝉悲鸣时》与《かまいたちの夜》等游戏被制作成系列作品,更积极对漫画、动画、电视剧等媒体发展。归类于视觉小说的成人游戏有像《Fate/stay night》、《ToHeart2》等超过10万套的畅销作品(PC-NEWSランキング调查),人气程度可见一斑。

另外,上面给出的示例并非最优解决方案,事实上,很多功能完全可以组件化配置(比如对话框,就可以使用笔者在LGame-Simple中所改进的LMessage,暂未移植而已……),还留待读者自行强化和完善。


————————擎天白玉柱、架海紫金梁————————

再说句题外话,刚才写这篇博文时把笔瘾勾起来了,将以前写了个开头的玄幻小说《山海演武传》丢到了Blog上,结果将相关系列博文分开了|||,想看此系列博文前几篇的大人们,后翻一页即可见……


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值