敌方生成

敌方单位生成器一直在屏幕的上方半随机地实例化敌机。

在Logic文件夹中新建Enemy文件夹,并在其中新建IEnemyCreatorEnemyCreatorEnemyCreatorMgr

EnemyCreatorMgr——读取当前关卡的配置LevelData、根据LevelData生成EnemyCreator、记录当前关卡已经生成的普通敌机的比例生成精英飞机,根据已经生成过的普通和敌机数量生成Boss敌机。读取所有敌机的配置、敌人的配置、敌机轨迹的配置;读取所有敌机的配置AllEnemyData,直接传给EnemyCreator

IEnemyCreator——敌机生成器地接口,因为Boss级敌机的行为可能和普通敌机的生成器不同,所以使用接口。

EnemyCreator——读取当前敌机的配置CreatorDataEnemyData,根据配置随机地生成相应的敌机。

Enemy——读取EnemyCreator传进来的EnemyData,使用这个配置数据执行每个敌机逻辑。

为了方便,我们让敌方直接生成为Camera的子物体,这样不用单独让它们向上移动了。使用EnemyCreatorMgr.SpawnCreator方法来生成每种敌方飞机的EnemyCreator

SceneRoot.Init中让Camera挂载EnemyCreatorMgr,因为SceneRoot.Init整个方法都是在加载界面中执行。我们也可以在这个方法中调用EnemyCreatorMgr.Init

Consts中添加ENEMY_PREFIX,用于设置指定的敌机图片名称,这个ENEMY_PREFIX使用string.Format来赋具体的值。

敌机和玩家所使用的Prefab一样,修改之前的Player这个Prefab,命名为Plane,并记住将Pathes修改。

敌机的Prefab使用对象池来生成,在PoolCfg脚本中添加敌机对象池配置。我们修改PoolMgrGameObjectPool,让它提供当前正在活跃的对象的数量,这样我们就可以判断当前已经生成了多少敌机,从而决定生成精英或Boss。

在Scripts——Logic——Enemy文件夹中新建Enemy继承PlaneView,这个脚本负责具体的敌机外观。

生成敌机基本逻辑

EnemyCreatorMgr中,先生成Normal敌机,判断Normal敌机生成一定数量之后,生成Elite敌机,当Normal敌机和Elite敌机都打完(躲开)后,生成Boss。

需要注意的是,生成敌机必须在GameStateModel.IsGaming是true的时候才能进行(在下面会添加),如果我们把生成方法直接写在EnemyCreatorMgr读取完配置数据的回调内,生成敌机时PoolMgr还没初始化完成。因为我们在PoolMgr中使用了异步方法,而且还延时100毫秒。

敌方配置工具

敌机的图片放在Resources——Picture——Enemy文件夹下,Pathes中定义好路径,敌方飞机根据名称分为三种,Elites、Item、Normal

在Logic文件夹内新建Data文件夹,在其中新建EnemyCreatorConfigData使用这个类来配置敌方飞机生成器。

EnemyCreatorConfigData文件中声明了配置类CreatorData,它提供了每一个敌机生成器的配置信息。

EnemyCreatorConfigData文件中声明了配置类LevelData,它提供了每一个关卡内的敌机信息。

Enums中添加EnemyType枚举

在Editor文件夹内新建GenerateLevelConfig,使用这个类来生成敌方配置的json文件。配置文件的Path略

正常情况下需要将Excel转Json,这里简单全都写在类里了。

敌方生成的队形

制作一个EnemyConfig.json,像Player的配置差不多,配置一下敌机的生命值和开火速度等信息,并在Pathes中指定好路径。

对应这个配置文件,在Logic——Data文件夹内新建一个配置类AllEnemyData

我们在EnemyCreatorMgr中读取EnemyConfig.json。

和之前读取json配置不同,这里读取json配置直接都是使用的JsonMapper.ToObject,这是因为我们定义了AllEnemyDataLevelData等配置类,之前使用JsonReader是不需要配置类直接读取配置的。

每一个敌方自动生成在Plane层下,我们先不让它们挂载移动的组件(后面又加上了),让每个敌机在屏幕上方先生成,然后摄像机自己划过敌机。

我们首先在飞机的配置中引用轨迹,生成一个敌机队列时,根据飞机轨迹配置和第一个敌机的生成位置等参数(不同的曲线需要的参数不同)来将整个敌机队列的队形生成出来。

敌机队列轨迹配置*

在Logic——Data文件夹新建ITrajctoryData接口并声明StraightTrajectoryEnemyTrajectoryData

在Editor文件夹内新建GenerateTrajectoryData,自动生成轨迹配置,配置文件的路径略。

当配置虽然种类相同,但是具体关联不大时,我们可以在Enums里面定义多个枚举,通过不同的枚举返回不同的配置,比如我们定义过的EnemyType和这里的TrajectoryType

EnemyTrajectoryData里我们希望通过字典返回不同的轨迹类型的配置类数组。但是LitJson.JsonMapper.Tojson不支持字典对象,我们需要自定义LitJson字典类型的序列化方式。我们可以通过以下方式新建Json对象:

1
2
3
4
JsonData jsondata = new JsonData();
jsondata["a"] = new JsonData();//初始化a对象
jsondata["a"].Add(1);//这一步可以把a对象的值设为数组,此时的json内容为{"a":[1]}
jsondata["a"][0] = 2;//直接给a对象的数组赋值,注意没有上一步不能直接调用这一步,直接jsondata["a"][0] = 2;会报错表示没有实例化

在Utility文件夹中新建JsonUtil,用来序列化和反序列化C#的字典,注意这里只能序列化特定类型的字典。不同的字典需要写不同的方法。调用jsonData.Add(object)方法时,如果传入的object是string,jsonData会将内容完全转义,让内容变成完全的字符串,所以我们通过JsonData.Add(JsonMapper.ToObject(JsonMapper.ToJson(data)))来转化为json。

我们在EnemyCreatorMgr中读取EnemyTrajectoryConfig.json。将配置数据传给Creator

敌机队列的配置EnemyData中声明TrajectoryTypeTrajectoryID,前者表示当前敌机类型,后者表示当前敌机队列轨迹id,当id是-1时,表示这个敌机随机挑选一个轨迹生成队列。

EnemyTrajectoryData里我们希望通过不同的轨迹id返回不同的队列数据类,但是当id是-1的时候我们的队列数据又是随机选择的,那我们应该怎么能返回不确定结果呢,答案就是使用回调,我们在EnemyTrajectoryData中声明一个Func<StraightTrajectoryData>变量,在id等于-1和不等于-1时,这个func指向不同的方法。

我们现在有了轨迹数据(ITrajectoryData)还不够,还要把数据传递给轨迹逻辑类(ITrajectory),所以我们修改之前的ITrajectory.Init(float angle),修改成更通用的ITrajectory.Init(ITrajectoryData data),我们使用is关键字来判断具体的轨迹数据类即可。

轨迹逻辑类的工厂

ITrajectory接口作为轨迹计算的逻辑类,调用非常频繁,之前我们直接在BulletModel中不断地New它,这里我们创建一个工厂类,直接调用批量创建。

在Scripts——Module文件夹内新建Factory文件夹,并在其中新建TrajectoryFactory脚本

游戏暂停事件

当游戏暂停时,AudioSource并不会受到影响,我们在MsgEvent添加游戏暂停和恢复的事件。在GameStateModel发布这个事件。

AudioMgr中监听这些事件。注意这里的Repaly中判断了GameStateModel的Pause状态。

敌机移动碰撞

敌机的组件Enemy继承了PlaneView,这表示所有的敌机都会生成在Plane层下,敌机如果想要移动和Palyer一样需要挂载MoveComponent

敌机的生成队列有一定的轨迹(假如是直线),敌机的飞行方向是这个轨迹的负方向,我们在ExtendUtil中添加获取Vector2负方向的方法。

在敌机的EnemyTrajectoryConfig.json中,不能出现负数的角度(将它转换成钝角),否则敌机的生成队列的负方向就是向上了。

敌机和Player公用相同的Prefab,所以都算2D刚体,处理碰撞逻辑都是相似的。碰撞消息组件需要获取到IBullet接口,所以敌机也要挂载BulletMgr组件。同时还需要挂载IBehaviour组件,新建一个EnemyBehaviour

敌机Enemy需要自己管理自己的生命值等信息,这一点和Player的全局GameModel不一样,所以我们还需要在Logic——Component中添加LifeComponent。这里是以Mono组件的方式提供生命值。

Player入场动画

在Logic文件夹内新建Ani文件夹,在里面添加脚本来实现一些由脚本控制的动画。添加PlayerEnterAni,在里面使用DoTween来让Palyer从屏幕下方飞进来。需要注意的是,游戏一开始整个场景都在向上飞,我们的飞机飞进来的动画要加上在这个动画播放的时间内场景移动的距离,所以我们还需要读取一下相机的速度,用它乘时间来获取距离。

GameStateModel中添加布尔型标志位,命名为IsGaming,只有这个标志位为true时,我们的Player才会开始发射子弹。我们在DOTween的OnComplete回调中把它设为true,在BulletMgr.Fire这个协程函数里使用while来判断是否应该发射子弹。

PlayerView.InitComponent()中添加这个组件。