原文地址
终于到了学习一些好东西的时候!在这片文章中我将查看敌人以及它们是如何设置,穿越和被击倒.我们将查看我如何在地图JSON文件中创建一些JSON元数据来初始化地图中将会包含的敌人类型,这些敌人所使用的声音,在一波敌人中的不同敌人队伍.我们先高屋建瓴的看下我是如何构建我的数据和类.我们很快会着手实际的JSON数据.
我们首先着手Enemy Waves,因为另外两者相互之间联系多一些.这里的Enemy Wave数据并不从属于工程中的一个实际类,而是由类作为某种map/hash集合.例如,当玩家开始游戏且屏幕下方的第一波格子"生成"时,这波tile有一个id为"wave1".所以EnemyManager类将会遍历并说"OK,第一波需要产生,我需要使用哪组?"
{ "id": "wave1", "groups":"group1", },当第一波产生时,EnemyManager类将会检查这些"组(groups)",这些组对应于下面EnemyGroups的ids.因此'wave1'产生,EnemyManager前去找到id为'group1'的EnemyGroup并告诉它开始产生(spawn)
一个EnemyGroup是一个Enemy类(和子类)的集合,这些集合共享一套那张地图路点(waypoints).如果你在地图上有一条路,你可能只有一套数据路点数据,"所有的敌人走都这条路从(10,10)到(100,10)到..."那么上面的JSON数据对你的enemyWaves组定义就够了.但假如你在你的地图搞了两条路,所以会有不同的路点集合,一个集合用于在屏幕左边生成的(敌人)组,另外一集合用于在屏幕上方产生的(敌人)组.
{ "id": "wave1", "groups":"group1a,group1b", },在我 demo的第二张图中有两条路和两拨分开的敌人组(enemy groups).上面的JSON代码用于产生第一波.由group1a和group1b指定的EnemyGroup将会在第一波生成时生成.
所以我们有了对应于com.zf.objects.enemy.EnemyType.as的enemyTypes,对应于com.zf.objects.EnemyEnemyGroup.as的enemyGroups,并且在enemyGroups里,enemies对应于com.zf.objects.enemy.Enemy作为基类.我们看下一个从JSON里摘录出来的敌人类型.
{ "id": "enemyA", "name": "Enemy A", "klass": "EnemyA", "sounds": [ { "state": "onEscape", "soundId": "escape1" }, { "state": "onBanish", "soundId": "banish1" } ] },把一个EnemyType当做一个概述一个特定敌人子类的元数据.所有的敌人继承与Enemy基类.然而,上面的enemyA将使用一个Enemy类的特定子类com.zf.objects.types.EnemyA.as.由于那个"kclass"属性,我们将使用EnemyA而不是无聊而老套的Enemy.as类.尽管EnemyA继承Enemy,我们会大量使用那个类.我们稍后会看到.
所以,一个EnemyType定义允许我们为类型指定一个内部的id,一个为那种敌人展示给玩家的名称,一个为那个类型使用的类以及构成声音状态的不同声音.例如,我们将会看到在Enemy基类中,当Enemy从地图中逃逸,基类将会调用一个函数说"如果存在播放onEscape声音状态".所以,在EnemyA中我们将onEscape定义为escape1所以当enemyA从地图中逃逸时escape1将会播放.如果我不为敌人类型包含"onEscape"状态,那就什么都不会播放.你是对的,我肯定会为基类Enemy定义一个基本的声音集合,但是在这个教程中,我不会.
我们看下enemyGroups的定义:
{ "id": "group1", "name": "Group 1", "spawnDelay": "800", "wave": "wave1", "waypointGroup": "wpGroup1", "enemies": [ { "typeId": "enemyA", "level": 1 }, { "typeId": "enemyB", "level": 1 }, { "typeId": "enemyB", "level": 1 }, { "typeId": "enemyA", "level": 1 }, { "typeId": "enemyB", "level": 1 }, { "typeId": "enemyA", "level": 1 } ] },
所以这个EnemyGroup总共由6个相邻之间产生时间会差0.8s的敌人组成.若你想要一个"满"级(屏幕上充满了怪),你或许可以定义30个产生间隔为100毫秒的敌人!你或许也想你所有的组都是相同的EnemyType,或者混合的类型...不管你想怎么做.把所有这些信息都放在这个JSON文件里的好处就是你的设计师,或者你的关卡编辑器或者测试玩家可以在一个文本编辑器里打开这个文件并修改每一波敌人的数量,产生间隔,敌人的类型等等.之后他们保存文件刷新浏览器.这将节省他们很多时间方便他们对于这些设置进行自我微调直到找到较好的平衡点.这个也节省了你很多时间,因为你无须硬编码使得每次在关卡设计师要敌人波出现的快一点就要你从新编译一个新的swf.你不知道是那个200ms快一些还是500ms快一些所以你拆分差值为375ms的时候它实际上想要快一秒.你会发现折腾了4次之后他会说"能把它整的快一秒吗?"有了文件中的数据,每个人可以负责他们自己对程序的微调.
好了!说够了没有意义的JSON数据.我们着手AS3!不管怎么说,那才是我们到这里搞的东西.
我的敌人图像来自一张可以在这里找到的精灵序列图.我已经联系了Flipz,创建这个tilesheet的美术童鞋,他很爽快地让我借用他的图像.
这个是整个的Enemy类文件.我们接下来会一步步的学习它.我将尝试减少到100行的代码段使得它容易消化,请记住我的格式化是jacked up所以我们再每一行能看到更多,我的文档块已被移除以便节省空间.
package com.zf.objects.enemy { import com.zf.core.Assets; import com.zf.core.Config; import com.zf.core.Game; import com.zf.states.Play; import com.zf.ui.healthBar.HealthBar; import org.osflash.signals.Signal; import starling.display.MovieClip; import starling.display.Sprite; public class Enemy extends Sprite { public static const ENEMY_DIR_UP:String = 'enemyUp'; public static const ENEMY_DIR_RIGHT:String = 'enemyRight'; public static const ENEMY_DIR_DOWN:String = 'enemyDown'; public static const ENEMY_DIR_LEFT:String = 'enemyLeft'; public static const ENEMY_SND_STATE_BANISH:String = "onBanish"; public static const ENEMY_SND_STATE_ESCAPE:String = "onEscape"; // set default speed to 1 public var speed:Number = 1; public var maxHP:Number = 10; public var currentHP:Number = 10; public var reward:int = 5; public var damage:int = 1; public var isEscaped:Boolean = false; public var isBanished:Boolean = false; public var willBeBanished:Boolean = false; public var onDestroy:Signal; public var onBanished:Signal; public var type:String = "Enemy"; public var uid:int; public var totalDist:Number; protected var _animState:String; protected var _animData:Object; protected var _animTexturesPrefix:String; protected var _soundData:Object; protected var _distToNext:Number; protected var _currentWPIndex:int; protected var _waypoints:Array = []; protected var _healthBar:HealthBar; // enemy speed * currentGameSpeed protected var _enemyGameSpeed:Number; protected var _enemyGameSpeedFPS:int; protected var _enemyBaseFPS:int = 12; /** * This is an Enemy's "currentHP" factoring in all bullets currently * flying towards it. So if this is <= 0, the enemy may still be alive, * but bullets have already been spawned with it's name on it */ protected var _fluxHP:Number; public function Enemy() { uid = Config.getUID(); _setInternalSpeeds(); _setupAnimData(); _changeAnimState(ENEMY_DIR_RIGHT); pivotX = width >> 1; pivotY = height >> 1; _soundData = {}; onDestroy = new Signal(Enemy); onBanished = new Signal(Enemy); // let enemy listen for game speed change Config.onGameSpeedChange.add(onGameSpeedChange); } public function init(wps:Array, dist:Number):void { isEscaped = false; isBanished = false; willBeBanished = false _waypoints = wps; totalDist = dist; _distToNext = _waypoints[0].distToNext; // clear the old animState _animState = ''; // set new animState _changeAnimState(_waypoints[0].nextDir); // reset WP index _currentWPIndex = 0; x = _waypoints[0].centerPoint.x; y = _waypoints[0].centerPoint.y; // reset current and _flux back to maxHP currentHP = _fluxHP = maxHP; _healthBar = new HealthBar(this, currentHP, maxHP, 30); _healthBar.x = -20; _healthBar.y = -10; addChild(_healthBar); }
public function update():void { _distToNext -= _enemyGameSpeed; totalDist -= _enemyGameSpeed; if(_distToNext <= 0) { _getNextWaypoint(); } switch(_animState) { case ENEMY_DIR_UP: y -= _enemyGameSpeed; break; case ENEMY_DIR_RIGHT: x += _enemyGameSpeed; break; case ENEMY_DIR_DOWN: y += _enemyGameSpeed; break; case ENEMY_DIR_LEFT: x -= _enemyGameSpeed; break; } } public function takeDamage(dmgAmt:Number):void { Config.log('Enemy', 'takeDamage', uid + " is taking " + dmgAmt + " damage"); if(!isEscaped) { Config.totals.totalDamage += dmgAmt; currentHP -= dmgAmt; if(_healthBar) { _healthBar.takeDamage(dmgAmt); } if(currentHP <= 0) { _handleBanished(); } Config.log('Enemy', 'takeDamage', "Enemy " + uid + " has " + currentHP + " hp remaining"); } } public function onGameSpeedChange():void { _removeAnimDataFromJuggler(); _setInternalSpeeds(); _setupAnimData(); _changeAnimState(_animState, true); } public function willDamageBanish(dmg:Number):Boolean { // deal damage to _fluxHP to see if this // damage amount will banish enemy _fluxHP -= dmg; if(_fluxHP <= 0) { willBeBanished = true; } Config.log('Enemy', 'willDamageBanish', "Enemy " + uid + " _fluxHP " + _fluxHP + " and willBeBanished is " + willBeBanished); return willBeBanished; } public function setSoundData(soundData:Array):void { var len:int = soundData.length; for(var i:int = 0; i < len; i++) { _soundData[soundData[i].state] = soundData[i].soundId; } } protected function _getNextWaypoint():void { _currentWPIndex++; if(_currentWPIndex < _waypoints.length - 1) { _changeAnimState(_waypoints[_currentWPIndex].nextDir); _distToNext = _waypoints[_currentWPIndex].distToNext; } else { _handleEscape(); } } protected function _handleEscape():void { isEscaped = true; _playIfStateExists(ENEMY_SND_STATE_ESCAPE); } protected function _handleBanished():void { Config.log('Enemy', '_handleBanished', "Enemy " + uid + " is below 0 health and isBanished = true"); isBanished = true; onBanished.dispatch(this); _playIfStateExists(ENEMY_SND_STATE_BANISH); }
protected function _playIfStateExists(state:String):void { if(_soundData.hasOwnProperty(state)) { Game.soundMgr.playFx(_soundData[state], Config.sfxVolume); } } protected function _setupAnimData():void { //_animState = ''; // removed due to a speed change bug _animData = {}; _animData[ENEMY_DIR_UP] = new MovieClip(Assets.ta.getTextures(_animTexturesPrefix + '_t_'), _enemyGameSpeedFPS); _animData[ENEMY_DIR_RIGHT] = new MovieClip(Assets.ta.getTextures(_animTexturesPrefix + '_r_'), _enemyGameSpeedFPS); _animData[ENEMY_DIR_DOWN] = new MovieClip(Assets.ta.getTextures(_animTexturesPrefix + '_b_'), _enemyGameSpeedFPS); _animData[ENEMY_DIR_LEFT] = new MovieClip(Assets.ta.getTextures(_animTexturesPrefix + '_l_'), _enemyGameSpeedFPS); } protected function _changeAnimState(newState:String, forceChange:Boolean = false):void { // make sure they are different states before removing and adding MCs // unless foreceChange is true if(_animState != newState || forceChange) { // _animState == '' on subsequent play throughs since init doesn't get called again if(_animState != '') { _removeAnimDataFromJuggler(); } _animState = newState; _addAnimDataToJuggler() } } protected function _removeAnimDataFromJuggler():void { // remove the old MovieClip from juggler Play.zfJuggler.remove(_animData[_animState]); // remove the old MovieClip from this Sprite removeChild(_animData[_animState]); } protected function _addAnimDataToJuggler():void { // add the new MovieClip to the Juggler Play.zfJuggler.add(_animData[_animState]); // add the new MovieClip to the Sprite addChild(_animData[_animState]); } protected function _setInternalSpeeds():void { _enemyGameSpeed = Config.currentGameSpeed * speed; _enemyGameSpeedFPS = int(Config.currentGameSpeed * _enemyBaseFPS); // make sure _enemyGameSpeedFPS is at least 1 if(_enemyGameSpeedFPS < 1) { _enemyGameSpeedFPS = 1; } } protected function _updateSpeed(spd:Number):void { // change speed from child classes speed = spd; // Call _setInternalSpeeds to reset internal speeds _setInternalSpeeds(); } public function destroy():void { onDestroy.dispatch(this); onDestroy.removeAll(); _removeAnimDataFromJuggler(); removeChild(_healthBar); _healthBar = null; removeFromParent(true); Config.log('Enemy', 'destroy', "+ + " + uid + " destroyed"); } } }
现在我们讨论了基本的Enemy类,我们看下产生更多的敌人类是多么的简单
在这个教程/demo例子中,我创建了一些子类,它们都改变一些父类的属性.这是挺糟的OOP.如果我在子类中所做的全部事情(在本教程中)就是改变敌人的速度和材质(名称)前缀,我应该仅将这些传递到父类也就是Enemy类的构造函数.var enemy:Enemy = new Enemy(speed,texturePrefix);之后照那样做.然而,我选择了子类路线是因为我在我正在搞的其他个人游戏项目里已经正在搞有新功能的子类.所以,就是在想把它搞出来的这个过程中我意识到对于这个特定的demo来说这是一个特定的/不必要的层级结构(hierarchy)
package com.zf.objects.enemy.types { import com.zf.objects.enemy.Enemy; public class EnemyA extends Enemy { public function EnemyA() { super(); // speed for EnemyA _updateSpeed(1.2); } override protected function _setupAnimData():void { _animTexturesPrefix = 'enemies/enemyA'; super._setupAnimData(); } } }是的.就是它!所有的我们敌人子类都可以是那么简短的一个文件.感谢OOP
package com.zf.objects.enemy.types { import com.zf.objects.enemy.Enemy; public class EnemyB extends Enemy { public function EnemyB() { super(); } override protected function _setupAnimData():void { _animTexturesPrefix = 'enemies/enemyB'; super._setupAnimData(); } } }在这个文件中我做的就更少了.EnemyB只是继承Enemy的默认速度1.0.这使得EnemyA比EnemyB稍快些.
随着Enemy类(何其子类)已经解决,我们看下汇聚在一块帮助Enemy的各个部分.我们将看下EnemyType和EnemyGroup.as在我们完成了这两者文件后,我们将会看下EnemyManager.as来把所有东西联系到一块,之后我们就能出去喝杯啤酒了.
EnemyType.as(com.zf.objects.enemy.EnemyType.as)是一个从表面看起来只是一个数据对象的相当小的文件.但它在这里有很重要的原因并且由于我从元表里的字符串加载类的方式,没有这个游戏就不可能....
package com.zf.objects.enemy { import com.zf.objects.enemy.types.EnemyA; import com.zf.objects.enemy.types.EnemyB; public class EnemyType { public var id:String; public var name:String; public var fullClass:String; public var soundData:Array; public function EnemyType(i:String, n:String, fC:String, sD:Array) { id = i; name = n; fullClass = 'com.zf.objects.enemy.types.' + fC; soundData = sD; } // create dummy versions of each enemy type, these will never be used private var _dummyEnemyA:EnemyA; private var _dummyEnemyB:EnemyB; } }
EnemyGroup.as(com.zf.objects.enemy.EnemyGroup.as)有点复杂,但仍旧在100行代码一下.
package com.zf.objects.enemy { import com.zf.core.Config; import com.zf.utils.GameTimer; import flash.events.TimerEvent; import org.osflash.signals.Signal; public class EnemyGroup { public var id:String; public var name:String; public var spawnDelay:Number; public var wave:String; public var waypointGroup:String; public var enemies:Array; public var enemyObjects:Array; public var spawnTimer:GameTimer; public var onSpawnTimerTick:Signal; public var onSpawnTimerComplete:Signal; public var isFinished:Boolean; public function EnemyGroup(i:String, n:String, wpGroup:String, sD:Number, w:String, e:Array) { id = i; name = n; waypointGroup = wpGroup; spawnDelay = sD; wave = w; enemies = e; enemyObjects = []; isFinished = false; spawnTimer = new GameTimer(id, spawnDelay, enemies.length); spawnTimer.addEventListener(TimerEvent.TIMER, _onSpawnTimer); spawnTimer.addEventListener(TimerEvent.TIMER_COMPLETE, _onSpawnTimerComplete); // dispatches the enemy being spawned onSpawnTimerTick = new Signal(); onSpawnTimerComplete = new Signal(); } public function startGroup():void { spawnTimer.start(); } public function pauseGroup():void { if(!isFinished && spawnTimer.running) { spawnTimer.pause(); } } public function resumeGroup():void { if(!isFinished && spawnTimer.paused) { spawnTimer.start(); } } private function _onSpawnTimer(evt:TimerEvent):void { onSpawnTimerTick.dispatch(enemyObjects.pop(), waypointGroup); } private function _onSpawnTimerComplete(evt:TimerEvent):void { isFinished = true; } public function destroy():void { Config.log('EnemyGroup', 'destroy', "+++ EnemyGroup Destroying"); spawnTimer.removeEventListener(TimerEvent.TIMER, _onSpawnTimer); spawnTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, _onSpawnTimerComplete); spawnTimer = null; var len:int = enemyObjects.length; for(var i:int = 0; i < len; i++) { enemyObjects[i].destroy(); } enemies = null; enemyObjects = null; Config.log('EnemyGroup', 'destroy', "--- EnemyGroup Destroyed"); } } }
package com.zf.managers { import com.zf.core.Config; import com.zf.core.Game; import com.zf.objects.enemy.Enemy; import com.zf.objects.enemy.EnemyGroup; import com.zf.objects.enemy.EnemyType; import com.zf.states.Play; import com.zf.utils.GameTimer; import flash.events.TimerEvent; import flash.utils.getDefinitionByName; import org.osflash.signals.Signal; import starling.display.Sprite; import starling.events.Event; import starling.extensions.PDParticleSystem; public class EnemyManager implements IZFManager { public var play:Play; public var onEnemyAdded:Signal; public var onEnemyRemoved:Signal; public var endOfEnemies:Signal; public var enemiesLeft:int; public var activeEnemies:int; public var delayCount:int = 0; public var onSpawnWave:Signal; private var _enemies:Array; private var _canvas:Sprite; // Holds the current map's enemy groups private var _enemyGroups:Object; // Holds the current map's enemy types private var _enemyTypes:Object; private var _enemyWaves:Object; public function EnemyManager(playState:Play) { play = playState; _canvas = play.enemyLayer; _enemies = []; activeEnemies = 0; onEnemyAdded = new Signal(Enemy); onEnemyRemoved = new Signal(Enemy); endOfEnemies = new Signal(); onSpawnWave = new Signal(String); _enemyGroups = {}; _enemyTypes = {}; _enemyWaves = {}; } public function update():void { if(_enemies.length > 0) { var e:Enemy; var len:int = _enemies.length; for(var i:int = len - 1; i >= 0; i--) { e = _enemies[i]; e.update(); if(e.isEscaped) { _handleEnemyEscaped(e); } } } } public function onGamePaused():void { _pauseGroups(); } public function onGameResumed():void { _resumeGroups(); } private function _pauseGroups():void { for each(var group:EnemyGroup in _enemyGroups) { group.pauseGroup(); } } private function _resumeGroups():void { for each(var group:EnemyGroup in _enemyGroups) { group.resumeGroup(); } }
public function destroyEnemy(e:Enemy):void { var len:int = _enemies.length; for(var i:int = 0; i < len; i++) { if(e == _enemies[i]) { Config.log('Enemy', 'destroyEnemy', "Destroying Enemy " + e.uid); _enemies.splice(i, 1); e.destroy(); e.removeFromParent(true); } } } private function _spawn(e:Enemy, wpGroup:String):void { var waypoints:Array = play.wpMgr.getWaypointsByGroup(wpGroup); var totalDist:Number = play.wpMgr.getRouteDistance(wpGroup); Config.totals.enemiesSpawned++; e.init(waypoints, totalDist); e.onBanished.add(handleEnemyBanished); _enemies.push(e); activeEnemies++; _canvas.addChild(e); onEnemyAdded.dispatch(e); } public function spawnWave(waveId:String):void { Game.soundMgr.playFx('ding1', Config.sfxVolume); onSpawnWave.dispatch(waveId); Config.changeCurrentWave(1); for each(var groupName:String in _enemyWaves[waveId]) { _enemyGroups[groupName].startGroup(); } } private function _handleEnemyEscaped(e:Enemy):void { enemiesLeft--; activeEnemies--; Config.totals.enemiesEscaped++; Config.changeCurrentHP(-e.damage); destroyEnemy(e) onEnemyRemoved.dispatch(e); if(enemiesLeft <= 0) { endOfEnemies.dispatch(); } } public function handleEnemyBanished(e:Enemy):void { Config.log('EnemyManager', 'handleEnemyBanished', 'Enemy ' + e.uid + " is being destroyed"); enemiesLeft--; activeEnemies--; Config.totals.enemiesBanished++; Config.changeCurrentGold(e.reward); onEnemyRemoved.dispatch(e); destroyEnemy(e); if(enemiesLeft <= 0) { endOfEnemies.dispatch(); } }
最终,完成EnemyManager.as
public function handleNewMapData(data:Object):void { var type:Object, group:Object, enemyType:EnemyType, enemy:*; // Create all enemy types for each(type in data.enemyTypes) { _enemyTypes[type.id] = new EnemyType(type.id, type.name, type.klass, type.sounds); } Config.maxWave = 0; // Create enemy wave mappings for each(var wave:Object in data.enemyWaves) { _enemyWaves[wave.id] = []; if(wave.groups.indexOf(',') != -1) { var groups:Array = wave.groups.split(','); _enemyWaves[wave.id] = groups; } else { _enemyWaves[wave.id].push(wave.groups); } Config.maxWave++; } // Create all enemy groups for each(group in data.enemyGroups) { _enemyGroups[group.id] = new EnemyGroup(group.id, group.name, group.waypointGroup, group.spawnDelay, group.wave, group.enemies); _enemyGroups[group.id].onSpawnTimerTick.add(onGroupSpawnTimerTick); _enemyGroups[group.id].onSpawnTimerComplete.add(onGroupSpawnTimerComplete); } // Create all actual enemies for each(group in _enemyGroups) { for each(var enemyObj:Object in group.enemies) { // get the enemyType enemyType = _enemyTypes[enemyObj.typeId]; // Creates a new enemy type from the fullClass name var newEnemy:Enemy = new (getDefinitionByName(enemyType.fullClass) as Class)(); newEnemy.setSoundData(enemyType.soundData); enemiesLeft++; // push new enemy onto object array group.enemyObjects.push(newEnemy); } // reverse array group.enemyObjects.reverse(); } } public function onGroupSpawnTimerTick(e:Enemy, wpGroup:String):void { Config.log('EnemyManager', 'onGroupSpawnTimerTick', "onGroupSpawnTimerTick: " + e); _spawn(e, wpGroup); } public function onGroupSpawnTimerComplete(e:Enemy):void { Config.log('EnemyManager', 'onGroupSpawnTimerComplete', "onGroupSpawnTimerComplete: " + e); } public function destroy():void { Config.log('EnemyManager', 'destroy', "EnemyManager Destroying"); _enemyTypes = null; var group:Object; for each(group in _enemyGroups) { group.destroy(); } _enemyGroups = null; var len:int = _enemies.length; for(var i:int = 0; i < len; i++) { _enemies[i].destroy(); } _enemies = null; Config.log("EnemyManager", "destroy", "EnemyManager Destroyed"); } } }
这就好了...这是我代码里完整的有关敌人死亡(run-down)的部分!不要忘了查看下我写的HealthBar.他挺简单的,只是一些矩形绘制代码.
就之前一样,自由查看我AS3 Starling TD Demo的完整产品.
你也可以在srcview中找到所有使用的代码
或者你可以下载整个工程的压缩文件
或者你可以查看Bitbucket上的repo
感觉阅读,希望这个对你有帮助