终于到了学习一些好东西的时候!在这片文章中我将查看敌人以及它们是如何设置,穿越和被击倒.我们将查看我如何在地图JSON文件中创建一些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 "group1"
- 第三行-它有一个叫Group 1的名字,我知道,这名字是"太"有创意了
- 第四行-spawnDelay是在敌人数组中每产生一个敌人的间隔毫秒数.所以每0.8秒会产生一个敌人.
- 第五行-wave是对应于你目前还没看到但是叫做"enemyWaveData"的一些东西,它主要处理屏幕底部的敌人波拼贴(enemy wave tile).这个wave id允许这些wave tile类有得到这数据的能力所以我们无需复制这个信息.把这个当做wave tiles的外键,使用这个外键来获得特定的EnemyGroup名称,敌人的数量等等.
- 第六行-waypointGroup对应于我们在之前文章中看到的地图tile数据.这意味着这个EnemyGroup为wpGroup1创建的路点集合.
- 第七行-enemies是我们单个的敌人要定义的.每个在这个数组中列出的对象将会是一个实际的为该组在屏幕上显示的敌人.每个对象由"typeld"和"level"组成.老实说,我都不知道我为什么在这里整了个"level"参数.在这个demo中它什么也不做,但如果你以这为基础开发,这就是你可以创建相同类型的敌人但是可以有一些敌人比在组里的其他敌人更强壮/难打/高级
- 第九行-使用"typeId"属性,在这里其值为"enemyA"并到上面EnemyTypes部分查找id"enemyA".这就是说Enemy应该使用在EnemyType "enemyA"数据中的所定义的所有类型数据.
- 第十行-为这个家伙使用等级1的数据
- 十三到十四行-下个要产生的敌人将会是一个EnemyType "enemyB"(上面没有显示,但是在完整的地图JSON数据里有)
- 十六到十九行-我继续在这个EnemyGroup中再多定义4个敌人.在实际的地图数据文件中,敌人像这样定义在一行里面以便节省空间.
所以这个EnemyGroup总共由6个相邻之间产生时间会差0.8s的敌人组成.若你想要一个"满"级(屏幕上充满了怪),你或许可以定义30个产生间隔为100毫秒的敌人!你或许也想你所有的组都是相同的EnemyType,或者混合的类型...不管你想怎么做.把所有这些信息都放在这个JSON文件里的好处就是你的设计师,或者你的关卡编辑器或者测试玩家可以在一个文本编辑器里打开这个文件并修改每一波敌人的数量,产生间隔,敌人的类型等等.之后他们保存文件刷新浏览器.这将节省他们很多时间方便他们对于这些设置进行自我微调直到找到较好的平衡点.这个也节省了你很多时间,因为你无须硬编码使得每次在关卡设计师要敌人波出现的快一点就要你从新编译一个新的swf.你不知道是那个200ms快一些还是500ms快一些所以你拆分差值为375ms的时候它实际上想要快一秒.你会发现折腾了4次之后他会说"能把它整的快一秒吗?"有了文件中的数据,每个人可以负责他们自己对程序的微调.
好了!说够了没有意义的JSON数据.我们着手AS3!不管怎么说,那才是我们到这里搞的东西.
Enemy.as
我的敌人图像来自一张可以在这里找到的精灵序列图.我已经联系了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);
}
- 16-19行-为单个敌人状态的方向定义常量,之后敌人就可以朝上,朝下,朝左,朝右.
- 21-22行-为可能的声音状态定义常量.我这里只定义了两个.你可以很容易的添加onSpawn(产生时),onDamage(毁坏时)等并将这些值与之前在EnemyType "sounds"部分讨论的元数据匹配起来.
- 25-32行-有关这个敌人的不同公共状态
- 34行-onDestory是一个在这个敌人在"从舞台中删除,被释放掉,被当做垃圾回收掉"的状态下派发的Signal(信号量,开源模拟事件机制的一个库)
- 35行-onBanished是一个在这个敌人被杀死在从舞台移除之前派发的Signal.
- 65行-给这个Enemy类一个唯一的ID,在调试且你需要一个特殊的实例名称来记录谁被哪颗子弹击中时非常有用.
- 66行-设置之后由动画属性和x/y移动内部使用的速度.
- 68行-为这个敌人设置动画数据
- 69行-改变这个敌人的动画状态为"enemyRight",这只是一个我自己随意设置的值.
- 70-71行-设置这个敌人的中心点为width/2,height/2
- 75-76行-设置Signals派发一个敌人对象类型(set up the Signals to be dispatching an Enemy object type).
- 79行-为Config的onGameSpeedChange Signal添加一个侦听器以便敌人知道游戏速度是否变化.
- 82行-init()看起来有点像构造函数,但它正好在一个敌人生成之前被调用所以我们可以传入一个路点数组和路点路线距离总和
- 83-85行-将布尔值重置回默认
- 86-87行-使用传入的参数初始化_wayPoints和totalDist.
- 88行-设置第一个"到下一个路点的距离"作为到waypoint[0]的距离
- 97-98行-设置这敌人的x/y点为第一个路点的x/y
- 101-106行-设置currentHP,fluxHP和maxHP为相同的maxHP,之后创建我写的HealthBar(com.zf.ui.healthBar.HealthBar.as)
Enemy.as续
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);
}
- 1行-每个时间间隔(tick)内update都会被调用,这个更新敌人的位置和其他变量
- 2行-从到下一个点距离减去下一个速度增量
- 4行-从距离总和中减去下一个速度增量
- 6行-我们是否到达下一个路点
- 7行-的到下一个路点
- 10-26行-检测我们所在的动画状态并相应地更新x或y.例如,如果我们要往右走(ENEMY_DIR_EIGHT)那么我们想添加enemyGameSpeed(稍后见)到x以便我们在右边移动那么多像素.
- 29行-当这个敌人被子弹击中时takeDamage被调用.(同时)伤害值被传入
- 31行-如果该敌人已经退出(escaped)忽略伤害.
- 32行-如果没有退出,为我们跟踪的totals.totalDamage添加上伤害值
- 34行-从我们当前的生命值中减去伤害值
- 35行-如果我们有一个血条(有时候当敌人在遭受伤害但同时从舞台中(play)被移除时,血条为null.并没有跟踪到这个Bug,但是封装一个以防遇到)
- 36行-调用我们稍后将会看到的healthBar的takeDamage函数并传入伤害值.
- 39行-如果我们当前的血条低于0
- 40行-调用_handleBanished()函数来处理这坏敌人已经被干掉的这个事实
- 47行-当游戏速度变化时onGameSpeedChange()函数会被调用,注释应该会很好的解释这一行
- 48行-从juggler(starling中的概念)中移除老的数据
- 50行-重置内部数据
- 52行-重置动画数据
- 54行-重置动画状态将true作为第二个参数传入来强制清除动画状态
- 60-65行-从fluxHP减去伤害值,之后查看其是否小于等于零进而查看下次射击是否干掉这个敌人,如果是返回true
- 69行-还记得上面JSON数据EnemyType里的"sounds"数组吗?我们就是在这里处理那些东西,设置_soundData的state为key,sound ID为值.
- 75行-得到下一个路点
- 76行-增加当前路店的索引
- 78行-如果当前的路点索引小雨路点数组的长度,这个敌人仍旧活着(in play)
- 79行-将动画状态改为路点的nextDir属性.这些与在Enemy.as文件顶部的路点方向常量相一致.
- 80行-设置新路点的distToNext为到下一个路点的距离
- 82行-如果敌人到达了路点数组的终端,调用_handleEscape()
- 87行-将isEscaped设置为true,因为这个家伙已经离开
- 88行-由于这个敌人逃离了,如果我们为这家伙设置了"onEscape"声音状态,那么就播放它.
- 93行-设置isBanished为true,因为这家伙我已被我们狂暴的防御技能烧焦或者烤死了.
- 94行-派发该敌人被杀死并肩该敌人作为数据传给Signal,所以别的类能确切地知道那个敌人死亡,并不会在检查死去的怪上费时间.
- 95行-如果我们定义了一个"onBanish"声音状态,那么就播放它.
结束Enemy.as
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");
}
}
}
- 2行-如果我们的_soundData定义了这个状态...
- 3行-调用我们SoundManager类的playFX函数传入要播放的sound id和来自Config中的sfxVolume来实际的播放声音
- 7行-这个创建了我们的动画数据.这个又是另外一种我从别人网站上发现的技术!我希望我之前能够把这些东西都放入收藏夹以便给予感谢!
- 8-9行-清除状态与动画数据
- 11行-_animData[ENEMY_DIR_UP] = new MovieClip(Assets.ta.getTextures(_animTetxturePrefix + '_t_'),_enemyGameSpeedFPS);
- 11行(再来一发)-所以我们将通过方向状态键使用一系列MovieClips填充_animData对象.为在这里精确地设置(exact setup)我特地命名了我的文件.例如,所有敌人往屏幕上方移动的动画都标示为:"enemies/enemyA_t_01,"enemies/enemyA_t_02","enemies/enemyA_t_03",和"enemies/enemyA_t_04",这样我可以确切地处理我们在此看到的东西.我要是早点指出来这个就更棒了,但实际上_animTexturesPrefix在这个类中还没有初始化.就像我们在下一个文件中看到的这取决于子类来设置那些.就OOP(面向对象)理论来说这个不错,但是执行起来就不是那么好了.EnemyA.as设置"_animTexturesPrefix = 'enemies/enemyA';",所以你会看到EnemyA将调用enemies/enemyA部分且这将添加_t_所以所有这4个文件都将由Assets.ta.getTextures挑选出来.之后我为MovieClip的FPS属性传入了_enemyGameSpeedFPS.我添加这部分是因为如果_enemyGameSpeedFPS是一个常量的话当你加速游戏速度为原来两倍的话,动画仍旧在6fps循环,以双倍速度运行时这个看起来很怪,所以稍后我将指出那.
- 17行-当需要改变动画状态时调用,这个负责改变并排序juggler和动画
- 20行-如果我们想将状态切换为与当前存在的状态完全一致,如果他们全完相同我们不想浪费循环从juggler添加和删除东西.然而,不过我们传入forceChange==true,那我们就想不论任何情况下更改数据.
- 23行-_animState将会是这样:如果还没有执行这些函数并将自身建立,所以如果_animState=''那么就无需从juggler移除任何东西.
- 24行-否则从juggler移除动画数据
- 26行-设置动画状态为新状态
- 28行-将新的动画数据添加到juggler
- 33-36行-注释已经总结了其意思,从juggler一处旧的动画数据之后将那个MovieClip从舞台移除.
- 40-43行-注释已经总结了其意思,将新的动画数据添加到juggler,之后添加MovieClip到舞台.
- 47行-设置_enemyGameSpeed(用于从x/y加减来实际移除Sprite)为currentGameSpeed(0,5,1,2)城西我们敌人的速度属性
- 48行-设置_enemyGameSpeedFPS(用于设置MovieClip速度)为currentGameSpeed(0.5,1,2)城西我们敌人的基础FPS属性并确保它是一个偶数.
- 50-52行-如果FPS不知怎么地小于1,设置它为1
- 55行-updateSpeed在子类中被调用.例如,EnemyA.as调用_updateSpeed(1.2)所以子类可以设置父类的速度属性,之后调用setInternalSpeeds以便传播(propagates)出去FPS速度等等.我也可以只从子类调用速度之后调用_setInternalSpeeds()来达到这种效果,但我只是想一个函数来处理这两件事.
- 62行-最终我们将干掉敌人
- 63行-发布我们正在干掉这个敌人的消息
- 64行-删除任何Signal的侦听器
- 65-69行-删除动画数据,删除血条,删除自身
现在我们讨论了基本的Enemy类,我们看下产生更多的敌人类是多么的简单
OOP笔记
在这个教程/demo例子中,我创建了一些子类,它们都改变一些父类的属性.这是挺糟的OOP.如果我在子类中所做的全部事情(在本教程中)就是改变敌人的速度和材质(名称)前缀,我应该仅将这些传递到父类也就是Enemy类的构造函数.var enemy:Enemy = new Enemy(speed,texturePrefix);之后照那样做.然而,我选择了子类路线是因为我在我正在搞的其他个人游戏项目里已经正在搞有新功能的子类.所以,就是在想把它搞出来的这个过程中我意识到对于这个特定的demo来说这是一个特定的/不必要的层级结构(hierarchy)
EnemyA.as
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
- 8行-调用Enemy的构造函数
- 10行-调用来自Enemy.as的_updateSpeed()函数并给它传入参数1.2
- 14行-就为改变/设置_animaTexturePrefix变量我重写了_setupAnimData()函数之后我调用父类的_setupAnimData()来实际的做所有的工作
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
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;
}
}
- 14-17行-挺简单的参数设置
- 16行-小心...我刚刚给fullClass传递了这种EnemyType的完全限定名称(fully qualified class name for this EnemyType)
- 21-22行-PROTIP(有言在先的意思?)!这里是允许你从JSON数据中的字符串创建类的关键.Flash的编译器对于要包含的文件是这么挑剔以致如果你在项目文件结构里包含了EnemyA.as,或者甚至你在50个文件里确切地写入了"import com.zf.objects.enemy.types.EnemyA;",除非至少有一个类定义为类型EnemyA,它从不会包含你的文件这样你将在运行碰到错误就是Flash不知道你在讨论的是什么类.但如果你导入了文件呢?Flash不在乎.所以在你游戏的一个地方你使用你的敌人类型定义了一个变量并且Flash会包含这些类.如果我再加10个敌人类型...EnemyC,Enemy10,EnemyFlying等等,我将需要常返回这个文件并添加一句"private var _whatever:EnemySomeType;"因此当我导入那些类时Flash知道我的意思.我不会马马虎虎地导入敌人类.我们是程序员.要修剪代码.
EnemyGroup.as
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");
}
}
}
- 26-31行-从传入的JSON元数据设置类变量
- 32行-这将持有(hold)我所有的敌人对象,现在将其初始化为一个空对象
- 33行-这组还没有开始,将isFinished设置为false
- 35行-创建一个我添加的可被暂停/返回的新的GameTimer Timer对象.这个来自与别人的博客.我在几年前学会了这手,但我听确信它是基于这里的LocalToGlobal的ExtenedTimer.我给他一个timer id,传入我的spawnDelay(产生延迟)并根据我数组中敌人的数量设置timer重复的次数.
- 36-37-行侦听标准的timer事件
- 40-41-行为GameTimer运转到(ticks)和其完成准备一些Signals
- 45行-当着一组准备产生(spawning),这个函数被调用开启了这个租的spawnTimer
- 49-51-行如果这个timer没有完成并且spawnTimer正在运行,告诉spawnTimer暂停.
- 55-57行-如果这个timer没有完成并且spawnTimer被暂停,告诉spawnTimer再次开始(返回).
- 61行-当spawnTimer运行到间隔(ticks)派发一个现在该产生一个新Enemy的事件,之后为其传入enemyObjects里弹出(pop)的游戏对象并传入他的waypointGroup信息.小的TimmyEnemy已经为初学者滑雪道做好了准备它需要它的通行证...送他上路.
- 65行-spawnTimer已完成并完成(has finished and is completed),设置isFinished = true;如果你的游戏设置为已打单第一波完成产生(spawning),你就能够产生第二波,那这个逻辑到这里可能就够了(that logic might go here)...另一个要派发的signal能够使下一波机制或者别的成为可能
- 68行-销毁这个敌人组(enemy group)
EnemyManager.as
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();
}
}
- 44行-获得我们指向Play State的局部索引
- 45行-设置我们的_canvas为Play的enemyLayer Sprite
- 46行-设置_enemies为空数组
- 47行-这个是构造函数,当前舞台上还没有活动的敌人,所以将其设置为0
- 49-52行-分别为当我们添加敌人到舞台,当从舞台移除敌人,当干掉了我们的所有敌人(hit the end of our enemies),当我们产生新的一波时设置Signals
- 59行-在游戏的更新间隔(update tick)时被调用
- 60行-如果在你的敌人数组里有任何敌人
- 63行-因为在敌人数组中索引较小的敌人可能会已经逃逸所以逆向循环数组,所以在我们将要冲刺通过容易的东西时(sprint through all the easy stuff)我们碰到了if isEscaped判断语句.似乎像给我的任何解释那样好(seems as good as any explanation to me).我认为前置循环和后支循环都没有什么关系.或许这里能得到一些循环性能的(提升).
- 65行-为数组中的每个敌人调用Enemy.update()函数
- 66行-如果敌人已逃逸,则处理逃掉的敌人
- 74行-当游戏暂停时调用...
- 78行-当游戏返回时调用...
- 83行-这个暂停所有的组产生timer(all group spawn timer)
- 89行-这个返回(resume)所有的组产生timer
继续EnemyManager.as
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();
}
}
- 1行-当我们需要销毁一个敌人时调用它
- 2-8行-获得我们敌人数组的长度,循环敌人数组查找我们传入的敌人实例,当找到后将其从数组中splice出来,销毁它将其从父容器移除,对其斩草除根,让其曝晒在阳光下(stake it through the heart,leave it in the sunlight)...我不知道,不管怎样销毁掉Enemy就好了(i dunno,whatever destorys the enemy for good)
- 13行-当要产生一个Enemy的时候调用它!传入要产生的Enemy和路点组id(waypoint group id)
- 14-15行-通过从Play State的WaypointManager传入路点组id得到路点和总距离
- 17行-增加enemiesSpawned的总和
- 19行调用Enemy.init()并传入路点和距离
- 21行为敌人的onBanished Signal添加一个回调侦听函数
- 23行-将我们的Enemy添加到敌人数组
- 25行-增加我们的activeEnemies计数器
- 27行-添加敌人到舞台
- 29行-在我们添加了一个新的敌人并且其在舞台上处于活跃状态的情况下派发onEnemyAdded signal
- 32行-spawnWave实际上是当一个WaveTile完成了其行程到达屏幕左边"倒计时产生下一波"时从远处(from way over)HUDManager作为一个回调函数调用
- 33行-当新一波产生时派发ding1声音
- 33行-派发我们产生了新一波
- 37行-增加波数计数器
- 40行-遍历波中的所有组并开始组产生timer
- 44行-当敌人逃亡时我们要做的任何事情,在我们调用destoryEnemy()之前像减少生命值等等发生在这里.
- 45-46行-减少enemiesLeft和activeEnemies计数器
- 48行-增加总共逃逸的敌人计数器
- 50行-根据敌人对我们的伤害减少自身的当前生命值(currentHP)
- 51行-最后销毁敌人
- 52行-派发敌人被销毁的signal
- 55行-如果enemiesLeft小于等于0,派发我们结束了我们的敌人(hit the end of our enemies)
- 59-73行-与handleEnemyEscaped非常相似除了我们添加了enemiesBanished并更新了我们当前的金钱
最终,完成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");
}
}
}
- 1行-在地图开始前调用以便EnemyManager能够处理新的地图数据
- 9行-遍历JSON数据中的所有"enemyTypes"并创建一个新的EnemyType并将其保存在我们的_enemyTypes对象
- 12行-设置maxWave为0因为将根据数据来增加他
- 16行-遍历所有的"enemyWaves"并在我们的对象中为每一个wave id创建一个新的条目
- 17行-如果"groups"参数含有逗号(",")那么我将那组拆分为数组并添加它
- 21行-否则将组名称放入wave id
- 23行-增加最大值maxValue
- 27行-遍历"enemyGroups"并创建所有的EnemyGroup对象并为signal添加侦听器
- 33行-遍历每个enemyGroup的敌人数组并创建本地图存在的每个敌人
- 39行-var newEnemy:Enemy = new (getDefinitionByName(enemyType.fullClass) as Class)();
- 39行-enemyType.fullClass看起来像是这个"com.zf.objects.enemy.types.EnemyA".这一行拿着类的全饰路径/名称并将其传入Flash的getDefinitionByName()来作为类,我们将其封装在小括号之后最终调用函数();.这个是那种"魔术"允许我们使用字符串来代表类名并且Flash将会很酷地为我们创建类.
- 40行-设置声音数据
- 42行-增加enemiesLeft
- 45行-为那个EnemyGroup将新的Enemy push进对象的enemyObjects数组.
- 49行-逆序enemyObjects数组以便最后创建的敌人实际上在数组的最后
- 55行-当到了一个group spawntimer的间隔,产生一个新的敌人
- 62行-销毁敌人管理器和任何遗留的敌人等.
这就好了...这是我代码里完整的有关敌人死亡(run-down)的部分!不要忘了查看下我写的HealthBar.他挺简单的,只是一些矩形绘制代码.
就之前一样,自由查看我AS3 Starling TD Demo的完整产品.
你也可以在srcview中找到所有使用的代码
或者你可以下载整个工程的压缩文件
或者你可以查看Bitbucket上的repo
感觉阅读,希望这个对你有帮助