淺談STG遊戲的開發(4月8日更新,已補全內容)
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow
也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!
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 20enemy MoveEnemy 75 20sleep 20enemy MoveEnemy 85 20sleep 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;//請注意,該類直接繼承的STGObjectpublic 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 20enemy MoveEnemy 75 20sleep 20enemy MoveEnemy 85 20sleep 20begin actionsleep 15enemy BeeEnemy 57 0sleep 15enemy BeeEnemy 59 0sleep 50enemy BeeEnemy 155 0sleep 50enemy BeeEnemy 155 0sleep 50enemy BeeEnemy 155 0sleep 50enemy BeeEnemy 155 0sleep 50enemy BeeEnemy 155 0sleep 50enemy BeeEnemy 155 0endcall actioncall 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 天》了……