动作游戏(ACT),作为在N早以前的红白机时代,便已经大量普及的经典游戏类型,它强调玩家的反应能力和手眼的配合,属于快节奏游戏的典型。相对而言,动作游戏的剧情一般也比较简单,主要通过熟悉操作技巧就可以进行游戏,多以2D为主潮流。在游戏中,玩家控制的角色不仅可以走、跑、跳,甚至可以俯身、爬行、翻滚、飞行、甚至爬墙(见附图:《忍者龙剑传》)。
ACT游戏的武器也多种多样,近程、远程、定时,还可以使用大量的辅助道具,譬如:吊索(例如《蜘蛛侠》)。加之种种场景的地形效果,流沙、冰地、履带、齿轮等等,配合场景中的种种机关,如墙上的开关、灯控、钉板等等,使游戏过程千变万化。即使是3D仿真技术发达的今天,相信仍有不少玩家流连这类ACT游戏吧?
任何一款ACT游戏,从根本上说,都会设定有自己的游戏背景。策划人员在设计这部分内容时,可以充分发挥自己的想象力。游戏的背景可以为数千年前的原始神话时代的《火之鸟》、《彩虹岛》,也可以是神秘奇幻的中世纪《圆桌武士》、《恶魔城》、《波斯王子》,甚至回到雄壮激荡的中国古代,作出《天地吞食》、《杨家将》,乃至于未来时代的《魂斗罗》,或者以著名故事、漫画为原型的《蜘蛛侠》、《梦幻岛》等,乃至于从来没有过的时间、空间、物种。故事需要尽可能的离奇或是感人,这样才能有效地抓住玩家,事实证明,在这样的游戏中,完全可以在游戏过程中加入情节的,譬如《恶魔城·月夜狂想曲》就是一个非常好的例子,在ACT为主的游戏过程中,角色会遇到很多人,为他提供信息,在一定条件下给予其道具,可以中途买卖物品。相信随着游戏的发展,这类多元化游戏将会越来越受欢迎。
而在游戏设计中,操作感也非常重要,矛头直指游戏的上手性。在PC或Android上玩这类游戏,更是对操作要求极高,游戏玩家玩的是否舒服,也全在这一步。举个例子,如我们控制一个角色去跳过一个很宽的沟。步骤应该为:跑——>快跑——>跳跃。初步设定“跑”“加速”“跳跃”必须设到“S”“D”“F”三个键上,通常情况就应该依次把“S”“D”“F”三个相邻的键设为“加速”“跑” “跳跃”。这样,平时玩家在游戏过程中,可以始终按住“D”键不放,需要加速时,无名指按着“S”,过沟大跳跃时,只需要食指点下“F”键就可以了,除此之外,其它的排列都将很别扭,可自己一试。
另外从键盘和鼠标的功能来说,有的东西已经是成规范性的东西了。对应相关的游戏软件我们也可以找到规律,并力图靠近它。做为策划,永远也不要霸道的要求玩家去适应你的规律,找到大众都喜欢都适应的规律才是为策划之正道。
牵扯到ACT游戏,自然免不了会有关卡的概念,所谓关卡就是游戏过程中一个又一个的小节。在这里也是同样可以发挥策划的想象力的好地方,每一关可以设计不同的特色,我想大家一定不会忘记古旧而精典的ACT游戏《魂斗罗》吧,记得履带+下砸钉锤那一关么?记得最后一关会飞起来的毒蝎么?这就是有特色而让人难忘的精典呀。
这一部分可以说是ACT游戏的核心。
至于游戏的规则设定,比如在什么情况下,角色会死亡?在什么情况下,敌NPC会OVER?角色在什么状态下可以躲过敌人飞行道具?角色在情况下会飞行?每一种道具的作用?都是需要设计者费心的,比如有的游戏,在规则上会设计一些独创的内容,如上面提到的《天地吞食》,某角色击打时如果同时按着“下”,就会把击打对象举起来并抛出。
而说起ACT游戏的代表作,那么便首推《超级马里奥》,这款家喻户晓的动作游戏,不但在当时,甚至对后世游戏界产生了无法估量的巨大影响。
截止到2008年,马里奥游戏分别在不同的机种上发布了116种,当中包括马力奥自己为主角的游戏和其他如任天堂大乱斗等游戏上,成为了吉尼斯世界纪录大全中,发行最多款电子游戏的电子游戏角色。
那么,对于这款地球人都知道的游戏,我们可不可以将它移植到Android之上,甚至再加上少许变化,令它成为一款全新的游戏呢?答案是肯定的。
相关知识点说了很多。下面,我将分别给出PC以及Android两个版本的马里奥类游戏实现,让大家对基本业务流程有一个初步了解。
源码下载地址:http://code.google.com/p/loon-simple/downloads/list
PC版实现:
package org.loon.game.test; import java.awt.Color; import java.awt.Graphics2D; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.util.Iterator; import java.util.LinkedList; import org.loon.framework.game.simple.GameScene; import org.loon.framework.game.simple.core.Deploy; import org.loon.framework.game.simple.core.LTimer; import org.loon.framework.game.simple.core.LTimerContext; import org.loon.framework.game.simple.core.Screen; /** * * 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:[email protected] * @version 0.1 */ public class Main extends Screen { public static final int WIDTH = 320; public static final int HEIGHT = 480; private LTimer delay; private Map map; private Hero hero; private ActionKey goLeftKey; private ActionKey goRightKey; private ActionKey jumpKey; private double x, y; // 键盘事件记录器 class ActionKey { static final int PRESS_ONLY = 1, STATE_RELEASED = 0, STATE_PRESSED = 1, STATE_WAITING_FOR_RELEASE = 2; int mode, amount, state; public ActionKey() { this(0); } public ActionKey(int mode) { this.mode = mode; reset(); } public void reset() { state = STATE_RELEASED; amount = 0; } public void press() { if (state != STATE_WAITING_FOR_RELEASE) { amount++; state = STATE_PRESSED; } } public void release() { state = STATE_RELEASED; } public boolean isPressed() { if (amount != 0) { if (state == STATE_RELEASED) { amount = 0; } else if (mode == PRESS_ONLY) { state = STATE_WAITING_FOR_RELEASE; amount = 0; } return true; } return false; } } public Main() { delay = new LTimer(20); goLeftKey = new ActionKey(); goRightKey = new ActionKey(); jumpKey = new ActionKey(ActionKey.PRESS_ONLY); initialize(); } private void initialize() { map = new Map("map.txt"); hero = new Hero("hero.gif", 192, 32, 40, 40, map); } public void alter(LTimerContext timer) { if (delay.action(timer.getTimeSinceLastUpdate())) { if (goLeftKey.isPressed()) { // 向左移动 hero.left(); } else if (goRightKey.isPressed()) { // 向右移动 hero.right(); } else { // 暂停 hero.stop(); } if (jumpKey.isPressed()) { // 跳跃 hero.jump(); } // 更新角色状态 hero.update(timer.getTimeSinceLastUpdate()); // 遍历精灵集合 LinkedList sprites = map.getSprites(); for (Iterator it = sprites.iterator(); it.hasNext();) { Sprite sprite = (Sprite) it.next(); // 更新所有精灵状态 sprite.update(timer.getTimeSinceLastUpdate()); // 碰撞测试 if (hero.isCollision(sprite)) { // 遇敌 if (sprite instanceof Enemy) { Enemy e = (Enemy) sprite; // 踩踏敌人 if ((int) hero.getY() < (int) e.getY()) { sprites.remove(e); hero.setForceJump(true); hero.jump(); break; } else { // 重新开始 initialize(); } // 食物 } else if (sprite instanceof Food) { Food f = (Food) sprite; sprites.remove(f); break; // 加速宝物 } else if (sprite instanceof Speed) { sprites.remove(sprite); Speed speedUp = (Speed) sprite; speedUp.use(hero); break; // 跳跃宝物 } else if (sprite instanceof Jump) { sprites.remove(sprite); Jump jump = (Jump) sprite; jump.use(hero); break; } } } } } /** * 绘制游戏界面 */ public void draw(Graphics2D g) { g.setColor(Color.BLACK); g.fillRect(0, 0, getWidth(), getHeight()); int offsetX = Main.WIDTH / 2 - (int) hero.getX(); offsetX = Math.min(offsetX, 0); offsetX = Math.max(offsetX, Main.WIDTH - map.getWidth()); int offsetY = Main.HEIGHT / 2 - (int) hero.getY(); offsetY = Math.min(offsetY, 0); offsetY = Math.max(offsetY, Main.HEIGHT - map.getHeight()); map.draw(g, offsetX, offsetY); hero.draw(g, offsetX, offsetY); LinkedList sprites = map.getSprites(); for (Iterator it = sprites.iterator(); it.hasNext();) { Sprite sprite = (Sprite) it.next(); x = Map.pixelsToTiles(sprite.getX()); y = Map.pixelsToTiles(sprite.getY()); // 精灵绘制限制在可见区域内 if (x > map.getFirstTileX() && x < map.getLastTileX() && y > map.getFirstTileY() && y < map.getLastTileY()) { sprite.draw(g, offsetX, offsetY); } } } public void leftClick(MouseEvent arg0) { } public void middleClick(MouseEvent arg0) { } public void rightClick(MouseEvent arg0) { } public void onKey(KeyEvent e) { int key = e.getKeyCode(); if (key == KeyEvent.VK_LEFT) { goLeftKey.press(); } if (key == KeyEvent.VK_RIGHT) { goRightKey.press(); } if (key == KeyEvent.VK_UP) { jumpKey.press(); } } public void onKeyUp(KeyEvent e) { int key = e.getKeyCode(); if (key == KeyEvent.VK_LEFT) { goLeftKey.release(); } if (key == KeyEvent.VK_RIGHT) { goRightKey.release(); } if (key == KeyEvent.VK_UP) { jumpKey.release(); } } public static void main(String[] args) { GameScene frame = new GameScene("火影忍者", WIDTH, HEIGHT); Deploy deploy = frame.getDeploy(); deploy.setScreen(new Main()); deploy.setShowFPS(true); deploy.setLogo(false); deploy.setFPS(100); deploy.mainLoop(); frame.showFrame(); } }
Android版实现:
package org.loon.framework.android.game.act; import java.util.Iterator; import java.util.LinkedList; import org.loon.framework.android.game.LAGraphics; import org.loon.framework.android.game.LAScreen; import org.loon.framework.android.game.LTimerContext; 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:[email protected] * @version 0.1 */ public class ScreenTest3 extends LAScreen { public static final int WIDTH = 320; public static final int HEIGHT = 480; // private LTimer delay; private Map map; private Hero hero; private ActionKey goLeftKey; private ActionKey goRightKey; private ActionKey jumpKey; private double x, y; // 键盘事件记录器 class ActionKey { static final int PRESS_ONLY = 1, STATE_RELEASED = 0, STATE_PRESSED = 1, STATE_WAITING_FOR_RELEASE = 2; int mode, amount, state; public ActionKey() { this(0); } public ActionKey(int mode) { this.mode = mode; reset(); } public void reset() { state = STATE_RELEASED; amount = 0; } public void press() { if (state != STATE_WAITING_FOR_RELEASE) { amount++; state = STATE_PRESSED; } } public void release() { state = STATE_RELEASED; } public boolean isPressed() { if (amount != 0) { if (state == STATE_RELEASED) { amount = 0; } else if (mode == PRESS_ONLY) { state = STATE_WAITING_FOR_RELEASE; amount = 0; } return true; } return false; } } public ScreenTest3() { // delay = new LTimer(20); goLeftKey = new ActionKey(); goRightKey = new ActionKey(); jumpKey = new ActionKey(ActionKey.PRESS_ONLY); initialize(); } private void initialize() { map = new Map("map.txt"); hero = new Hero("hero.gif", 192, 32, 40, 40, map); } public void alter(LTimerContext timer) { // PS:虚拟机速度较慢,实机需要开放此部分,否则动作过快 // if (delay.action(timer.getTimeSinceLastUpdate())) { if (goLeftKey.isPressed()) { // 向左移动 hero.left(); } else if (goRightKey.isPressed()) { // 向右移动 hero.right(); } else { // 暂停 hero.stop(); } if (jumpKey.isPressed()) { // 跳跃 hero.jump(); } // 更新角色状态 hero.update(timer.getTimeSinceLastUpdate()); // 遍历精灵集合 LinkedList<Sprite> sprites = map.getSprites(); Iterator<Sprite> it = sprites.iterator(); while (it.hasNext()) { Sprite sprite = it.next(); // 更新所有精灵状态 sprite.update(timer.getTimeSinceLastUpdate()); // 碰撞测试 if (hero.isCollision(sprite)) { // 遇敌 if (sprite instanceof Enemy) { Enemy e = (Enemy) sprite; // 踩踏敌人 if ((int) hero.getY() < (int) e.getY()) { sprites.remove(e); hero.setForceJump(true); hero.jump(); break; } else { // 重新开始 initialize(); } // 食物 } else if (sprite instanceof Food) { Food f = (Food) sprite; sprites.remove(f); break; // 加速宝物 } else if (sprite instanceof Speed) { sprites.remove(sprite); Speed speedUp = (Speed) sprite; speedUp.use(hero); break; // 跳跃宝物 } else if (sprite instanceof Jump) { sprites.remove(sprite); Jump jump = (Jump) sprite; jump.use(hero); break; } } } // } } public void draw(LAGraphics g) { g.setColor(Color.BLACK); g.fillRect(0, 0, getWidth(), getHeight()); int offsetX = WIDTH / 2 - (int) hero.getX(); offsetX = Math.min(offsetX, 0); offsetX = Math.max(offsetX, WIDTH - map.getWidth()); int offsetY = HEIGHT / 2 - (int) hero.getY(); offsetY = Math.min(offsetY, 0); offsetY = Math.max(offsetY, HEIGHT - map.getHeight()); map.draw(g, offsetX, offsetY); hero.draw(g, offsetX, offsetY); LinkedList<Sprite> sprites = map.getSprites(); for (Iterator<Sprite> it = sprites.iterator(); it.hasNext();) { Sprite sprite = it.next(); x = Map.pixelsToTiles(sprite.getX()); y = Map.pixelsToTiles(sprite.getY()); // 精灵绘制限制在可见区域内 if (x > map.getFirstTileX() && x < map.getLastTileX() && y > map.getFirstTileY() && y < map.getLastTileY()) sprite.draw(g, offsetX, offsetY); } } public boolean onKeyDown(int keyCode, KeyEvent e) { int key = e.getKeyCode(); if (key == KeyEvent.KEYCODE_DPAD_LEFT) { goLeftKey.press(); } if (key == KeyEvent.KEYCODE_DPAD_RIGHT) { goRightKey.press(); } if (key == KeyEvent.KEYCODE_DPAD_UP) { jumpKey.press(); } return false; } public boolean onKeyUp(int keyCode, KeyEvent e) { int key = e.getKeyCode(); if (key == KeyEvent.KEYCODE_DPAD_LEFT) { goLeftKey.release(); } if (key == KeyEvent.KEYCODE_DPAD_RIGHT) { goRightKey.release(); } if (key == KeyEvent.KEYCODE_DPAD_UP) { jumpKey.release(); } return false; } public boolean onTouchDown(MotionEvent e) { return false; } public boolean onTouchMove(MotionEvent e) { return false; } public boolean onTouchUp(MotionEvent e) { return false; } }
源码下载地址:http://code.google.com/p/loon-simple/downloads/list
——————————————————————————————
搞Java的同僚们,有空想着去http://www.java.com/en/store/intl-contact.jsp 申请2010年开通中国区的商店服务吧,别最后搞得和Android Market似的,只能托人“洗钱”……
这是待发的AVG示例截图: