浅谈STG游戏的开发(4月8日更新,已补全内容)

PS:从根本上讲,弹幕游戏本就归属于STG,或者说也仅仅是种STG罢了。因此,本文也可以视作在介绍LGame中任意STG类游戏的基本开发。

通常我们所谓的弹幕,词源来自英文的【barrage】,本来是指英国陆军在1915年一战期间,采取的特种战术名称。该战术宗旨为,无差别的不间断发射,组成上天下地的半圆形交织火力网,消灭攻击范围内可能存在的一切敌人。(不过因为消耗过巨,兼之敌我不分,此战术在一战后就不再使用。小弟额外一提,目前各国研发的【金属风暴】系统其实都和它有异曲同工之妙,不过定点性更强了,因此也有人管该系统叫【弹幕墙】)


但具体到中文的【弹幕】二字,则和【达人】【素颜】之类一样,都源于东瀛日本的平假名名词(当然,因为日本文化源自汉唐,所以也可以视为汉文化的回流~)。因此,目前市面上比较流行的具体弹幕类游戏,如东方系列,如虫姬系列,如式神之城系列,又如三国战记系列,都是日式弹幕游戏,他们大多都具有一定程度上的AVG式剧情交互,并且单纯的射击而已。

而一般来说,我们开发游戏的最终目地,是让游戏者玩的开心高兴,并非打击他们的游戏乐趣,更不是想要彻底杀死他们(^^)。

所以在开发弹幕类游戏时,我们就不能,也不可能像军用的【barrage】系统一样真不给人半点生机;相反的,我们还要让玩家【有机可乘】,能够不被那么轻易的消灭掉才行。故而绝大多数的弹幕类游戏,都结合有“射击”与“闪避”两大游戏要素。即要让玩家“在敌人放出的大量子弹(弹幕)的细小空隙间闪避”,“并且能够予以敌人猛烈的还击”,这样才能给以玩家弹幕时的独特快感。

因此,对绝大多数的弹幕游戏而言,通常会有以下共性存在:


1、敌人的子弹速度比普通的射击游戏的慢很多。

2、大量的敌弹会以一定的算法有规则地射出,往往在画面上排出几何形状,必定有空隙存在。

3、敌弹的攻击判定和自机的被击中判定比眼见的小很多,不那么容易射中我方目标。

4、有可能突然减慢,或突然增加自机速度,使精密的避弹更容易操作。

当然,弹幕类游戏中敌方的子弹,毕竟会比普通的射击游戏密集许多,所以不论敌弹判定还是自机被击中判定时有多“放水”,弹幕游戏的难度,也大多会比一般的射击类游戏更高些。

至于小弟下面例举的具体弹幕示例源码,则直接采用LGame自带的STG扩展包开发,所以在默认状态下已经支持了触屏与键盘操作(只要继承了STGHero类,我们的主角机就能够完成相关操作),以及基本的角色碰撞检查。所以,大家并不需要再关注什么额外的代码设定,仅仅理解下所谓弹幕,也不过是一堆形状在屏幕上做自定义碰撞,就足够了。


在STG扩展包中,继承了Screen类的STGScreen,用以显示游戏画面。而STGObject这个对象,则代表了全部的屏幕中机体(包括子弹,敌人,我方,物品等),至于区别它们关系的,则是预先定义好的,位于STGScreen类中的9种弹幕对象属性,即:

//主角
public static final int HERO = 0;
//主角子弹
public static final int HERO_SHOT = 1;
//敌人
public static final int ENEMY = 2;
//敌人子弹
public static final int ENEMY_SHOT = 3;
//该物体无法命中目标(纯漂过,不和任何对象发生作用)
public static final int NO_HIT = 4;
//物品
public static final int ITEM = 5;
//必须取得的物品(也就是任务物品,预留区域)
public static final int GET_ITEM = 6;
//自杀(与此物体碰撞即宣布游戏失败,预留区域)
public static final int SUICIDE = 7;
//全部命中,不分敌我
public static final int ALL_HIT = 8;


我们可以通过变更STGObject类的attribute参数,修正当前角色的作用。而STGScreen对象,则可以在所有的STGObject对象中调取stg变量获得,该对象对应着作为主窗体的STGScreen,我们可以通过该对象为中介,获得多个子类间的相互合作与调配。

下面小弟将例举一些实际的代码例子。

请注意,所有的角色类都是STGObject对象的衍生。所谓敌机,我方机体,或者等等,不过是继承了STGObject类的对象,设定了不同的attribute参数而已。所以从本质上看,他们都是一种东西。

设定一个主角类:
package org.loon.framework.javase.game.stg.test;

import org.loon.framework.javase.game.action.map.Config;
import org.loon.framework.javase.game.core.graphics.LColor;
import org.loon.framework.javase.game.stg.STGHero;
import org.loon.framework.javase.game.stg.STGScreen;

public class Hero1 extends STGHero {

    public Hero1(STGScreen stg, int no, float x, float y, int tpno) {
        super(stg, no, x, y, tpno);

        // 设定主角生命值(被击中60次后死亡)
        this.setHP(60);
        // 设定主角魔法值
        this.setMP(60);
        // 设定自身动画(第一项参数为动画顺序,第二项参数为对应的图像索引)
        this.setPlaneBitmap(0, 0);
        // 如果设定有多个setPlaneBitmap,可开启此函数,以完成动画播放
        // setPlaneAnime(true);
        // 设定动画延迟
        // setPlaneAnimeDelay(delay);
        // 设定自身位置
        this.setLocation(x, y);
        // 旋转图像为指定角度
        // setPlaneAngle(90);
        // 变更图像为指定色彩
        // setPlaneBitmapColor(LColor.red);
        // 变更图像大小
        // setPlaneSize(w, h);
        // 显示图像
        this.setPlaneView(true);
        // 设定子弹用类
        this.setHeroShot("Shot1");
        // 设定自身受伤用类
        // this.setDamagedEffect("D1");
        this.setHitW(32);
        this.setHitH(32);
    }

    public void onShot() {

    }

    public void onDamaged() {
        this.setPlaneBitmapColor(LColor.red);
    }

    public void onMove() {
        this.setPlaneBitmapColor(LColor.white);
        // stg对象即当前的当前STGScreen,所有子类都可以调取到这个对象。通过此对象为中介,
        // 我们获得STGScreen状态,也可以 获得多个子类间的相互合作与调配。
        // 根据角色所朝向的方向,变更角色图
        switch (stg.getHeroTouch().getDirection()) {
        case Config.LEFT:
        case Config.TLEFT:
            setPlaneBitmap(0, 1);
            break;
        case Config.RIGHT:
        case Config.TRIGHT:
            setPlaneBitmap(0, 2);
            break;
        default:
            setPlaneBitmap(0, 0);
            break;
        }
    }
}

设定对应的主角机子弹类:
package org.loon.framework.javase.game.stg.test;

import org.loon.framework.javase.game.stg.STGScreen;
import org.loon.framework.javase.game.stg.shot.HeroShot;

public class Shot1 extends HeroShot {

    public Shot1(STGScreen stg, int no, float x, float y, int tpno) {
        super(stg, no, x, y, tpno);
        //下列两参数为命中点偏移
        hitX = hitY = 2;
        //设定角色图像索引
        setPlaneBitmap(0, 3);
        setLocation(x + 14, y);
        //设定角色大小(如不设定,直接视为图像大小)
        setHitW(15);
        setHitH(15);
    }

}

设定一个最基本的敌人类(直接继承现有的敌兵类):
package org.loon.framework.javase.game.stg.test;

import org.loon.framework.javase.game.stg.STGScreen;
import org.loon.framework.javase.game.stg.enemy.EnemyOne;

public class MoveEnemy extends EnemyOne {

    public MoveEnemy(STGScreen stg, int no, float x, float y, int tpno) {
        super(stg, no, x, y, tpno);
        //使用图像索引5(对应图像的注入顺序)
        setPlaneBitmap(0, 5);
        //坐标位于脚本导入的坐标
        setLocation(x, y);
    }

    public void onExplosion() {
        
    }

}

然后以最简单的方式,进行如下脚本操作:
//设定反射用包
package org.loon.framework.javase.game.stg.test

//加载主角类
leader Hero1 166 266

//加载敌人
enemy MoveEnemy 55 20
//延迟20豪秒进行下一步操作
sleep 20
enemy MoveEnemy 75 20
sleep 20
enemy MoveEnemy 85 20
sleep 20

屏幕上就会得到这样的显示,这时已经可以进行最基本的游戏了。


事实上,如果以STG包开发弹幕游戏,那么我们真正需要关心的,仅仅是子弹、我方机体、敌方机体的行走算法,也就是如何让它们以尽量绚丽多彩移动的形式展现在用户眼前,而无需介怀其他什么。比如,大家可能都感觉到上图中敌人的直线运动太单调了,那么下面我们设定一个新类,并命名为BeeEnemy,然后做如下设定。
package org.loon.framework.javase.game.stg.test;

import org.loon.framework.javase.game.stg.STGObject;
import org.loon.framework.javase.game.stg.STGScreen;
import org.loon.framework.javase.game.stg.enemy.EnemyOne;
import org.loon.framework.javase.game.utils.MathUtils;

public class BeeEnemy extends EnemyOne {

    public BeeEnemy(STGScreen stg, int no, float x, float y, int tpno) {
        super(stg, no, x, y, tpno);
        this.setPlaneBitmap(0, 8);
        this.setPlaneBitmap(1, 9);
        this.setPlaneBitmap(2, 10);
        this.setPlaneBitmap(3, 11);
        this.setPlaneBitmap(4, 12);
        this.setPlaneAnime(true);
        this.setLocation(x, y);
        //死亡延迟时间为0,即命中足够次数后立刻消失
        this.setDieSleep(0);
        //移动速度3
        this.speed = 3;
        //命中三次后,敌人消失
        this.hitPoint = 3;
    }

    public float distance(float x1, float y1, float x2, float y2) {
        x1 -= x2;
        y1 -= y2;
        return MathUtils.sqrt(x1 * x1 + y1 * y1);
    }

    private int c;

    public void update() {
        super.update();
        if (getY() >= 50) {
            if (c == 0) {
                for (int i = 0; i < 360; i += 30) {
                    float rad = 2 * MathUtils.PI * ((float) i / 360);
                    STGObject bow = newPlane("BeeShot", getX() + 18,
                            getY() + 32, targetPlnNo);
                    bow.offsetX = MathUtils.cos(rad);
                    bow.offsetY = MathUtils.sin(rad);
                }
            }
            ++c;
            c %= 150;
        }
    }

    //如果敌人角色死后,将自动执行此函数
    public void onExplosion() {

    }

}

再给它添加一种专用子弹。
package org.loon.framework.javase.game.stg.test;

import org.loon.framework.javase.game.core.LSystem;
import org.loon.framework.javase.game.stg.STGObject;
import org.loon.framework.javase.game.stg.STGScreen;

//请注意,该类直接继承的STGObject
public class BeeShot extends STGObject {

    public BeeShot(STGScreen stg, int no, float x, float y, int tpno) {
        super(stg, no, x, y, tpno);
        //设定对象属性为敌方子弹
        this.attribute = STGScreen.ENEMY_SHOT;
        //图像索引为7
        setPlaneBitmap(0, 7);
        setLocation(x, y);
        hitX = hitY = 1;
    }

    public void update() {
        //每次移动时,按照偏移值的数值进行操作
        move(offsetX, offsetY);
        //如果角色被命中(就子弹来讲,也意味着命中目标),或者超出屏幕
        if (hitFlag || !LSystem.screenRect.contains(getX(), getY())) {
            //删除当前角色
            delete();
        }
    }

}

然后我们修改脚本,多增加一些操作:
//设定反射用包
package org.loon.framework.javase.game.stg.test

//加载主角类
leader Hero1 166 266

//加载敌人
enemy MoveEnemy 55 20
//延迟20豪秒进行下一步操作
sleep 20
enemy MoveEnemy 75 20
sleep 20
enemy MoveEnemy 85 20
sleep 20

begin action

sleep 15
enemy BeeEnemy 57 0
sleep 15
enemy BeeEnemy 59 0
sleep 50
enemy BeeEnemy 155 0
sleep 50
enemy BeeEnemy 155 0
sleep 50
enemy BeeEnemy 155 0
sleep 50
enemy BeeEnemy 155 0
sleep 50
enemy BeeEnemy 155 0
sleep 50
enemy BeeEnemy 155 0
end

call action
call action

就会得到如下图所示的游戏效果,子弹呈圆形喷射而出。



最后,我们还可以添加新类作为Boss:


package org.loon.framework.javase.game.stg.test;

import org.loon.framework.javase.game.stg.STGScreen;
import org.loon.framework.javase.game.stg.enemy.EnemyMidle;

public class Boss1 extends EnemyMidle {

    public Boss1(STGScreen stg, int no, float x, float y, int tpno) {
        super(stg, no, x, y, tpno);
        setPlaneBitmap(0, 6);
        setLocation((getScreenWidth() - getWidth()) / 2, y);
        setView(true);
        setHitPoint(60);
    }

    public void onExplosion() {

    }

    public void onEffectOne() {

    }

    int count;

    public void onEffectTwo() {
        count++;
        if (count % 5 == 0) {
            addClass("BossShot1", getX() + 32, getY() + 90, super.targetPlnNo);
        }
        if (count % 6 == 0) {
            addClass("BossShot1", getX() + 45, getY() + 90, super.targetPlnNo);
        }
        if (count % 10 == 0) {
            addClass("BossShot2", getX() + 32, getY() + 90, super.targetPlnNo);
        }
        if (count > 20){
            count = 0;
        }
    }

}

然后为它添加两种,一种是自定义的,一种是继承自默认子弹类的:
package org.loon.framework.javase.game.stg.test;

import org.loon.framework.javase.game.stg.STGObject;
import org.loon.framework.javase.game.stg.STGScreen;

public class BossShot1 extends STGObject {

    public BossShot1(STGScreen stg, int no, float x, float y, int tpno) {
        super(stg, no, x, y, tpno);
        super.attribute = STGScreen.ENEMY_SHOT;
        setPlaneBitmap(0, 7);
        setLocation(x, y);
        hitX = hitY = 1;
    }

    public void update() {
        move(0, 12);
        if (getY() > stg.getHeight()) {
            delete();
        }
    }

}
package org.loon.framework.javase.game.stg.test;

import org.loon.framework.javase.game.stg.STGScreen;
import org.loon.framework.javase.game.stg.shot.MoonShot;

public class BossShot2 extends MoonShot{

    public BossShot2(STGScreen stg, int no, float x, float y, int tpno) {
        super(stg, no, x, y, tpno);
        setPlaneBitmap(0, 13);
        setPlaneBitmap(1, 14);
        setPlaneBitmap(2, 15);
        setPlaneBitmap(3, 16);
        setPlaneAnime(true);
    }


}

这时在屏幕上我们就可以和Boss开打了,效果图如下所示:


package org.loon.framework.javase.game.stg.test;

import org.loon.framework.javase.game.GameScene;
import org.loon.framework.javase.game.core.graphics.LColor;
import org.loon.framework.javase.game.core.graphics.component.LButton;
import org.loon.framework.javase.game.core.graphics.opengl.GLEx;
import org.loon.framework.javase.game.core.input.LInputFactory.Key;
import org.loon.framework.javase.game.core.input.LTouch;
import org.loon.framework.javase.game.core.input.LTransition;
import org.loon.framework.javase.game.stg.STGScreen;

public class Test extends STGScreen {

    public LTransition onTransition() {
        return LTransition.newEmpty();
    }

    public Test(String path) {
        // 需要读取的脚本文件
        super(path);

    }

    public void loadDrawable(DrawableVisit bitmap) {

        // 注入图像到STGScreen(内部会形成单幅纹理,ID即插入顺序)
        bitmap.add("assets/hero0.png");
        bitmap.add("assets/hero1.png");
        bitmap.add("assets/hero2.png");
        bitmap.add("assets/shot.png");
        bitmap.add("assets/boom.png");
        bitmap.add("assets/ghost.png");
        bitmap.add("assets/boss.png");
        bitmap.add("assets/greenfire.png");
        bitmap.add("assets/bee.png", 48, 48);
        bitmap.add("assets/moon1.png");
        bitmap.add("assets/moon2.png");
        bitmap.add("assets/moon3.png");
        bitmap.add("assets/moon4.png");
        // 设定背景为星空图(绘制产生)
        setStarModeBackground(LColor.white);
        // 设定滚屏背景图片
        // setScrollModeBackground("assets/background.png");
        // 设定无背景(无设定时默认为此)
        // setNotBackground();
    }

    /**
     * 游戏脚本监听(返回true时强制中断脚本,也可于此自定义游戏脚本)
     */
    public boolean onCommandAction(String cmd) {
        return false;
    }

    /**
     * 指定的图像ID监听(用于渲染指定ID对应的图像)
     */
    public boolean onDrawPlane(GLEx g, int id) {
        return false;
    }

    /**
     * 游戏主循环(位于循环线程中)
     */
    public void onGameLoop() {

    }

    /**
     * 当脚本读取完毕时,将触发此函数
     */
    public void onCommandAchieve() {

    }

    /**
     * 当主角死亡时
     */
    public void onHeroDeath() {
        System.out.println("over");
    }

    /**
     * 当敌兵被清空时
     */
    public void onEnemyClear() {

    }

    public void onLoading() {
        LButton btn = new LButton("assets/button.png") {
            public void downClick() {
                setKeyDown(Key.ENTER);
            }

            public void upClick() {
                setKeyUp(Key.ENTER);

            }
        };
        bottomOn(btn);
        btn.setLocation(getWidth() - btn.getHeight() - 25, btn.getY() - 25);
        add(btn);
        // 禁止此按钮影响STG触屏事件
        addTouchLimit(btn);
    }

    public void onDown(LTouch e) {

    }

    public void onDrag(LTouch e) {

    }

    public void onMove(LTouch e) {

    }

    public void onUp(LTouch e) {

    }

    public void update(long elapsedTime) {

    }

    public static void main(String[] args) {
        GameScene game = new GameScene("弹幕测试", 320, 480);
        game.setShowFPS(true);
        game.setShowLogo(false);
        game.setScreen(new Test("assets/stage1.txt"));
        game.showScreen();
    }
}

而且同样的代码放到Android版中照旧通行,效果如下图所示(横屏竖屏也无所谓):



小弟在SVN的更新中发了两个版本(直接下LGame-0.3.3-Beta即可,解包可见),一个标准Java的,一个Android的,源码基本一致,所以不再赘述。(话说小弟C#版也写了,不过都发比较占空间,等发LGame正式版时再说了……)

另外,使用STG包之所以在图像资源使用上稍微麻烦一点,是因为它们在内部都被LGame压成了单独的纹理,以保证弹幕速度。等后期小弟提供IDE时,配置上就没这么繁琐了。(本来就有两种图像资源加载方式,一种是直接填文件名,一种是读xml配置,,只不过后者暂无工具不太好做~)

http://loon-simple.googlecode.com/svn/trunk
_____________

说点题外话,小弟发现国漫还是很有希望的,比如小弟刚看了两集《圣斗士星矢Ω》,就感觉这货已经越来越接近国漫……前两天看完《屌丝女士》系列再次剧荒,目前值得期待的,就剩下《神秘博士》与《SPEC 天》了……



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值