新手引导介绍
新手引导的目的
介绍游戏玩法,某些原创性的游戏玩法介绍非常重要。
指引付费,介绍某些资源的购买途径。
其他,某些特殊要求和介绍等。
强制引导最常见的引导方式。屏幕全变黑,留出一块区域强制玩家点击,并有点击提示动画。
优点
易规范化
制作简单,逻辑清晰
缺点
玩家体验一般,强制引导只能做有限步数 的引导,不能一直出现。
强制引导一般用在UI交互的引导,在游戏场景中需要根据不同游戏类型做出取舍。
开放式引导像任务列表一样,使用提示的方式告诉玩家应该执行的操作。开放式引导一般用在游戏场景中。
引导逻辑的设计方式引导部分需要独立于游戏的主逻辑部分。
强制引导分步进行,触发后一步一步地进行引导——使用Queue来储存行为数据。
设计一个行为接口,通过实现这个接口来定义不同的引导行为。
其余部分简要3
逻辑帧设定我们在EnemyCreatorMgr中使用IUpdate.UpdateFun()来选择合适的EnemyCreator并生成敌机,在EnemyCreator中使用IUpdate.UpdateFun()来确定当前的队列生成的最后一个飞机已经完全进入摄像机视野,这两个敌方并不需要每帧都更新。
我们在IUpdate接口中声明UpdateTime属性,每一个实现这个接口的对象都需要指定自己过多少帧更新一次,然后再声明Times属性,每一个实现这个接口的对象都需要单独记录目前已经过了多少帧。
我们修改LifeCycleAddConfig.LifeCycleFuncs,在其中添加判断逻辑。
修改完成之后,普通的IUpdate不需要对这两个属性赋值,在需要赋值的类里,直接赋值UpdateTime即可,Times属性永远不需要被赋值。
敌机飞出屏幕后回收在Enemy中添加JudgeBeyondBorder方法,并且添加LimitUpdate方法,敌机的回收也不需要每帧都判断。
生成星星敌机被毁后生成星星。
添加星星Prefab,记得加上CircleCollider2D Trigger和Path ...
其余部分简要2
敌方生成敌方单位生成器一直在屏幕的上方半随机地实例化敌机。
在Logic文件夹中新建Enemy文件夹,并在其中新建IEnemyCreator、EnemyCreator和EnemyCreatorMgr
EnemyCreatorMgr——读取当前关卡的配置LevelData、根据LevelData生成EnemyCreator、记录当前关卡已经生成的普通敌机的比例生成精英飞机,根据已经生成过的普通和敌机数量生成Boss敌机。读取所有敌机的配置、敌人的配置、敌机轨迹的配置;读取所有敌机的配置AllEnemyData,直接传给EnemyCreator
IEnemyCreator——敌机生成器地接口,因为Boss级敌机的行为可能和普通敌机的生成器不同,所以使用接口。
EnemyCreator——读取当前敌机的配置CreatorData和EnemyData,根据配置随机地生成相应的敌机。
Enemy——读取EnemyCreator传进来的EnemyData,使用这个配置数据执行每个敌机逻辑。
为了方便,我们让敌方直接生成为Camera的子物体,这样不用单独让它们向上移动了。使用EnemyCrea ...
其余部分简要1
这套课程实在太长了,而且代码一边写一边重构,全部做笔记意义不大,现将后面的部分做一个摘要目录,里面有意义的再着重整理成笔记。
子弹碰撞Collider2DComponent、IColliderMsg想要接受碰撞事件继承这个接口即可。碰撞方和接受方都会接受到对方的标签。
Collider2DComponent只能挂载在有RigidBody组件的对象上。Collider2DComponent会把对象自己和对方碰撞体身上所有实现了IColliderMsg的脚本全都触发。
在Logic文件夹中新建Collider2DMsg文件夹(在后面重构后被删除),在其中新建PlayerCollider2DMsg继承IColliderMsg,使用这个Mono脚本来处理Player的碰撞消息,用新的脚本把逻辑分离开。在PlayerView中动态挂载这个脚本和Collider2DComponent
添加BulletOwner枚举,防止自己的子弹打中自己,在IBulletModel文件里声明IBullet接口,让IBulletModel继承IBullet、然后PlayerBulletModel中添加好这个枚举 ...
子弹轨迹和重构发射逻辑
之前通过BulletMgr和Bullet脚本发射子弹复用性不高,我们需要重构一下这里的逻辑。
子弹的轨迹部分的逻辑也需要添加上。
子弹轨迹添加BulletConfig.json(它的Path略),并将GameConfig.json当中的子弹速度转移到这里来,然后添加子弹各个等级的偏移角度。
1234567891011{ "Player": { "bulletSpeed": 5, "trajectory": { "0": [ 90 ], "1": [ -30, 30 ], "2": [ -30, 90, 30 ], "3": [ -30, -60, 90, 60, 30 ] } }}
ITrajectory在Logic文件夹中新建Trajectory文件夹,并在其中新建ITrajectory
1234567891011121 ...
子弹生成
在Resources——Picture文件夹中新建Battle——Player文件夹。
将Player的子弹图片资源放到Player文件夹内。这里的子弹命名非常简单,直接是数字,每个数字对应一种飞机ID。
0号子弹的Pixels Per Unit :1000
1号子弹的Pixels Per Unit :600
2号子弹的Pixels Per Unit :300
3号子弹的Pixels Per Unit :500
创建一个Prefab,命名为Bullet,在其中挂载SpriteRenderer组件。将其放在Resources——Prefab——Game文件夹内。
准备在Player的Prefab中新建一个子节点,命名为“BulletRoot”,并调整一下它的位置,放在飞机头部,注意千万不要修改子节点的Z坐标(保持是0即可),否则它发出的子弹不会和其他飞机碰撞。
Pathes1234public const string PREFAB_BULLET = PREFAB_GAME_FOLDER + "Bullet";public const string BULLET_ ...
移动范围限制和飞机拖动
移动范围限制有关正交摄像机的Size和实际渲染尺寸的关系:
https://atao-blast.github.io/2022/02/24/UnityBook/Unity-Book-Chapter6Part1/
在2D游戏中,只要我们设置的pixel per Unit是100,那么摄像机在世界坐标系下渲染的相对大小就能计算出来(Camera也提供了aspect属性)。
然后我们根据相机的RectTransform的位置和上面的相对大小,就能求得当前摄像机所渲染的世界坐标位置边界。然后根据这个边界限制飞机的飞行范围。
飞机本身的大小,可以根据SpriteRenderer提供的bound属性求得,所有继承了Renderer的组件都提供bound属性,可以计算对象的包围盒在世界空间中的位置,如果对象在世界空间中一直在运动,那么这个属性也是一直在变化的。
GameUtil在里面提供获取当前摄像机视野边界的世界坐标的方法。
12345678910111213141516171819202122232425262728293031323334353637383940414243private st ...
移动功能组件
添加配置,修改InitPlane.json
1"planeSpeed": 0.1
MoveComponent在Logic文件夹内新建Component文件夹,在其中添加游戏中需要用到的泛用性强但是需要Mono脚本的组件。Component类似于Module,但是它需要依赖Mono。
1234567891011121314151617181920using UnityEngine;public class MoveComponent : MonoBehaviour{ private float _speed; public void Init(float speed) { _speed = speed; } public void Move(Vector2 dir) { if (_speed != 0) { transform.Translate(_speed * Time.deltaTime * dir); ...
Player实例化
Player实例化修改Player的Prefab的Y坐标,这里改为-2。
修改SceneRoot
12345678public class SceneRoot : MonoBehaviour{ public void Init() { //... var player = LoadMgr.Instance.LoadPrefabAndInstantiate(Pathes.PREFAB_PLAYER); player.AddComponent<PlayerView>(); }
PlayerView在Logic——View文件夹下新建PlayerView
12345678910111213141516171819202122232425262728293031323334353637using UnityEngine;public class PlayerView : PlaneView{ private readonly float _startY = -2.0f; ...
消息系统重构
重构消息系统,不以接口为单位发送消息,而是使用Action。这样用来减少发消息的粒度,想象一下,我们有一个控制类监听了WASD四个按键,但是这个控制类只有一个方法实现IReceiver接口,我们还需要在方法内判断按下的键是什么,这样有损性能。能不判断就不判断是程序底层设计时的优化原则之一。
直接使用Action多播,会有重复监听的问题,我们在最后实现一个ActionMgr,使用HashSet<Action<T>>来解决这个问题。
修改MessageSystem1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162using System;using System.Collections.Generic;public interface IMessageSystem{ void AddListener(int key, Action<object[]> callb ...