AIプログラムとかUnityゲーム開発について

探索や学習などを活用したAI系ゲームを作りたいと思います。

oneliner Rogue のソースを読む

こちらで紹介されていた一次元のローグのソースをダウンロードさせてもらったので、ソースを読んでおります。tsubakit1.hateblo.jp

これまでゾンビと戦うゲームとかドルアーガの塔もどきとかTowerDefenseとか作ってきましたが、
アイテムを取得して、それを利用したりするような処理を、全然作ってないんですよね。
特にtowerDefenseでは定番の砲台のバージョンアップが手付かず。
結局は、UIを作るのが面倒くさいということなんですが、
このへんで、ここを突破しないと、ネクロダンサーもどきも開発が進まないぞってことで、
このone liner rougeのソースが参考になると思っております。

チュートリアルはあるし、攻撃のエフェクトや、感情表現、ログWindowなど、必要な要素がきちんとそろっていますね。ここまで作りこめば、アプリとして公開できるレベルと思います。

ソースはこちらで公開されてるんですが、残念ながらassetがそろってないので動かすことはできないようです。

monkey coders'


大きく構造としては、

  • 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	"よくやった これで まくらをたかくして ねむれるし
ねるまえに しこたま おさけをのんでも だいじょうぶじゃ
ひゃっほー こよいは えんかいじゃあ!"