こちらで紹介されていた一次元のローグのソースをダウンロードさせてもらったので、ソースを読んでおります。tsubakit1.hateblo.jp
これまでゾンビと戦うゲームとかドルアーガの塔もどきとかTowerDefenseとか作ってきましたが、
アイテムを取得して、それを利用したりするような処理を、全然作ってないんですよね。
特にtowerDefenseでは定番の砲台のバージョンアップが手付かず。
結局は、UIを作るのが面倒くさいということなんですが、
このへんで、ここを突破しないと、ネクロダンサーもどきも開発が進まないぞってことで、
このone liner rougeのソースが参考になると思っております。
チュートリアルはあるし、攻撃のエフェクトや、感情表現、ログWindowなど、必要な要素がきちんとそろっていますね。ここまで作りこめば、アプリとして公開できるレベルと思います。
ソースはこちらで公開されてるんですが、残念ながらassetがそろってないので動かすことはできないようです。
大きく構造としては、
- Actor 役者の処理(プレイヤーとモンスターを含む)
- Enemy モンスター(クリチャー)
- Player 主人公(人間)
- Item アイテム(投げたり、装備できる)
- Floor 床(罠とかある)
このローグは、床に痺れや毒の罠があったりしてしますが、
Actorとして共通処理で書いてあります。
これはUnityの2D RogueLike tutorialのabstractと同じで、
Actorを継承して、PlayerとEnemyが記述される構造です。
床に関してはFloor、アイテムはItemで処理があります。
移動は一次元ですが、アイテムは、食べる、投げる、装備する、使うってことで、
基本的な処理がすべて入っています。
Jump移動(Actor.cs)
while( Time.time - tNow <= jumpDurationSec ) { float rate = (Time.time - tNow) / jumpDurationSec; Vector3 targetPos = Vector3.Lerp(pos, nextPos, rate); transform.position = new Vector3(targetPos.x, targetPos.y + Mathf.Sin ( Mathf.PI * rate ) * 2.0f, targetPos.z); yield return new WaitForEndOfFrame(); }
移動はコルーチンですが、ジャンプに関しては、縦にSinを使ってジャンプしてました。
なるほどサイン波で移動すればジャンプっぽいですね。
攻撃(Actor.cs)
if(TestAttackHit(target, armedWeapon)) { alive = ItemEntity.AttackBy(armedWeapon, target, this, 1.0f); effects.Spawn(AttackEffect, target.transform.position + effectOffset); while(target.gauge.IsAnimating) { yield return new WaitForEndOfFrame(); } if(!alive) { _BeatActor(target); yield return new WaitForSeconds(0.3f); } } else { target._ActivateDuck(); } SendMessage("OnPostAttack", SendMessageOptions.DontRequireReceiver); GetHungerAfterAction(ActionType.Attack); m_isOnActionNow = false; }
攻撃後は腹が減ります。敵が死んだかどうかがaliveに返るみたいですね
ActivateDuckは攻撃を避けた処理のようです
duckというのはいわゆる「アヒル」ですが、
自動詞に「ひょいと避ける」という意味があるようです。
アヒルが水にもぐるときの動作を指すようです。
こんな意味は全く知りませんでした。
この作者はかなり英語力があるのではないだろうか?w
アイテムを拾う
protected IEnumerator Action_PickupItemOnGround() { PickupItemOnGround(); yield return new WaitForSeconds(0.3f); GetHungerAfterAction(ActionType.GetItemOnGround); m_isOnActionNow = false; yield return null; } protected void PickupItemOnGround() { if(CanPickupItemOnGround()) { SendMessage("OnPrePickupItemOnGround", SendMessageOptions.DontRequireReceiver); Item item = step.itemOnStep; GUIManager.GetManager().Message(charName + " は " +item.ItemName + " を みつけた!" ); if( items.Count < kMAX_ITEM_CARRY ) { items.Add(item.ItemEntity); Destroy (item.gameObject); GUIManager.GetManager().Message(charName + " は " + item.ItemName + "を ひろった!" ); effects.PlaySE(this, ActorSE.ItemPickup); } else { GUIManager.GetManager().Message("だが " + charName + " は もちものが いっぱいだ!" ); } SendMessage("OnPostPickupItemOnGround", SendMessageOptions.DontRequireReceiver); } }
itemはlistで管理してますね。所持枠以内ならAdd
敵の行動処理(EnemyActor.cs)
protected override void PerformTurnAction(GameManager gm) { if(!IsVisible()) { m_isOnActionNow = false; return; } m_value = (m_value == 1) ? -1 : 1; Actor naborLeft = this.step.GetStep(-1).actorOnStep; Actor naborRight = this.step.GetStep(1).actorOnStep; if(naborLeft != this && naborLeft != null && naborLeft.race == Race.Human) { GUIManager.GetManager().DebugMessage("[Actor][Attack]" + gameObject.name + " attacks " + naborLeft.gameObject.name); StartCoroutine(Action_Attack (naborLeft, ElementType.ET_Physical)); } else if(naborRight != this && naborRight != null && naborRight.race == Race.Human) { GUIManager.GetManager().DebugMessage("[Actor][Attack]" + gameObject.name + " attacks " + naborRight.gameObject.name); StartCoroutine(Action_Attack (naborRight, ElementType.ET_Physical)); } else if(CanMove(m_value)) { StartCoroutine(Action_Move (m_value)); } else { m_isOnActionNow = false; } }
見えないなら動かない
右でも左でも隣にプレイヤーがいたら襲う
いない場合は、移動できるなら移動。
これ見る限り、モンスターはアイテムを拾ったり、ジャンプしたりはしないみたいですね
攻撃時のダメージ計算(Actor.cs)
public int CalcurateAttackDamage(ItemEntity e, Actor target, ElementType attackKind, float jumpCoeff) { float attack = ap[(int)attackKind]; float defence = target.def[(int)attackKind]; if( e != null ) { attack += e.ap[(int)attackKind]; } if( target.armedShield != null ) { defence += target.armedShield.def[(int)attackKind]; } float damage = (attack - defence) * jumpCoeff; return Mathf.FloorToInt(damage); }
アイテムがあるときは、攻撃力e.ap[(int)attackKind]が追加
敵が防具があると、防御力target.armedShield.def[(int)attackKind]追加
これはプレイヤー、モンスター共通なので、モンスターもアイテムを所持できる仕様ですね。
jumpCoeffは、ジャンプ攻撃での補正かな?
アイテムを投げる攻撃は、actor自体の攻撃力はなしで、アイテムの攻撃力だけの計算ですね。
ただ、attack*2.5fとあるので、actorの攻撃力が低い場合は得かも。
public int CalcurateThrowDamageOfItem(ItemEntity e, Actor target, ElementType attackKind) { float attack = e.ap[(int)attackKind]; float defence = target.def[(int)attackKind]; if( target.armedShield != null ) { defence += target.armedShield.def[(int)attackKind]; } float damage = attack * 2.5f - defence; return Mathf.FloorToInt(damage); }
マップに敵を配置(Map.cs)
5から9の間のランダム値を間隔で、1000マスのマップに敵を配置する。
この間隔は、-3から3で変動
スタートは0ですが、しばらくは安全地帯がありますね(initalNoEnemyZone)
private void _GenerateEnemy(string questName) { int length = steps.Length; int enemyPopGap = Random.Range (5, 10); int nEnemies = ((int) length / enemyPopGap ) + 1; int initialNoEnemyZone = m_enemyDB.GetMinimumEnemyStep(questName); for(int i = 0; i < nEnemies; ++i) { int popStep = enemyPopGap * i + Random.Range (-3, 3); popStep = Mathf.Clamp (popStep, 0, length-1); while (steps[popStep].actorOnStep != null && popStep < length) { ++popStep; } if( popStep >= length ) { Debug.Log ("[EnemyGen] max step reached. finishing enemy pop.. "); break; } if( popStep < initialNoEnemyZone ) { continue; } //Debug.Log ("[Enemy] pop at "+ popStep); GameObject eo = GameObject.Instantiate(m_enemyFab, Vector3.zero, Quaternion.identity) as GameObject; EnemyActor ea = eo.GetComponent<EnemyActor>(); ea.Initialize(questName, popStep, m_enemyDB, m_itemDB); steps[popStep].SetActor(ea); eo.transform.parent = m_enemyParent.transform; } }
クエストやモンスター、アイテムのデータは、エクセルファイルで管理している。
QuestData.xls
マップ上の位置で、おじさんが話しかけてくる。
ネタバレしてしまった
120 stepover TRUE {0} event "ちゃんとやっとるか ようすを みにきたぞ うむ なかなか じゅんちょうな ようじゃな" 200 stepover TRUE {0} event "このさきは おばけは でないぞよ きたみちを もどるのじゃ" 999 beatall:2 TRUE {0} endevent "よくやった これで まくらをたかくして ねむれるし ねるまえに しこたま おさけをのんでも だいじょうぶじゃ ひゃっほー こよいは えんかいじゃあ!"