Unity插件——ABC详解
ABC是啥?显然不是什么出生在美国的中国人,而是:Ability & Combat Toolkit
Ability & Combat Toolkit(简称 ACT 或 ABC)是一款专为 Unity 游戏引擎设计的战斗系统与技能框架插件,旨在帮助开发者快速构建复杂、动态的战斗机制和技能效果,无需从零编写底层代码。
今天我们就来学习这个插件的内容,了解这个插件是如何实现的战斗系统和技能框架。
我们根据代码的结构把这个插件分成这些系统:
🔥 核心层 (Core Layer)
├── 能力系统 ← 战斗的核心
├── 实体管理 ← 对象的基础
└── 投射物系统 ← 战斗的表现
🎮 控制层 (Control Layer)
├── 移动控制 ← 依赖实体管理
├── 动画系统 ← 依赖移动控制
└── 相机系统 ← 依赖角色状态
🎨 交互层 (Interaction Layer)
├── 输入管理 ← 连接控制层
└── UI系统 ← 反映核心层状态
🔧 辅助层 (Support Layer)
├── 武器系统 ← 扩展能力系统
├── 数据管理 ← 支持所有系统
└── 全局管理 ← 协调各系统
我们来一点一点学习:
核心层 (Core Layer)
能力系统
ABC能力系统是一个完整的Unity技能战斗框架,负责管理游戏中所有技能的定义、激活、执行和效果应用。它是整个战斗系统的核心,提供了从简单攻击到复杂魔法的完整解决方案。
功能实现
[System.Serializable]
public class ABC_Ability {/// <summary>/// Name of the Ability/// </summary>[Tooltip("Name of the Ability")]public string name = "";/// <summary>/// ID of the ability so name can be changed but all setups linking to this ability will remain/// </summary>[Tooltip("ID of the Ability")]public int abilityID = 0;/// <summary>/// If the ability is enabled or not. If false then the ability can't be used in game/// </summary>[Tooltip("Is the ability active in the game. If not active then can't be cast and pool is not made")]public bool abilityEnabled = true;
}
这段代码实现了技能系统的基础架构,通过[System.Serializable]标记使得整个技能类可以在Unity编辑器中进行可视化编辑和数据持久化。每个技能都拥有唯一的ID和名称,其中ID作为内部引用的主键,确保即使技能名称发生变化,其他系统的引用关系也不会断裂,这种设计模式在大型项目中非常重要,因为它提供了数据的稳定性和可维护性。abilityEnabled字段作为技能的总开关,控制技能是否可以在游戏中使用,当设置为false时,技能不仅无法激活,连对象池都不会被创建,这样可以有效节省内存资源。底层逻辑采用了面向对象的封装思想,将技能的所有属性和行为集中在一个类中管理,便于扩展和维护。
public bool Activate(ABC_IEntity Originator, bool IgnoreTriggerCheck = false, bool IgnoreActivationPermittedCheck = false, bool IgnoreComboCheck = false, bool IgnoreHoldPreparation = false) {//Check if ability can activate (has correct triggers and targets)if (this.CanActivate(Originator, IgnoreTriggerCheck, IgnoreActivationPermittedCheck, IgnoreComboCheck) == false)return false;//Stop weapon blockingOriginator.StopWeaponBlocking();//If true then the function will not use the hold to continue preparation functionalitythis.IgnoreHoldPreparation = IgnoreHoldPreparation;//Activate the abilityABC_Utilities.mbSurrogate.StartCoroutine(this.ActivateAbility(Originator));//If ability was interrupted and did not activate then return falseif (this.AbilityActivationInterrupted())return false;//ability activated ok so return truereturn true;
}
这段代码是技能激活的主要入口点,采用了防御式编程的设计模式,首先通过CanActivate()方法进行全面的前置条件检查,包括触发器验证、激活权限确认、连击条件检测等多个维度的验证,只有所有条件都满足时技能才会进入激活流程。在确认可以激活后,系统会立即停止角色的武器格挡状态,这是为了避免技能激活与防御动作产生冲突,确保角色状态的一致性。函数提供了多个可选参数来控制检查行为,这种设计允许在特殊情况下(如脚本强制激活、AI控制等)绕过某些检查限制。技能的实际执行通过StartCoroutine启动协程来完成,这样设计的好处是避免了阻塞主线程,让复杂的技能激活流程可以分帧执行,同时也便于实现中断机制。最后通过检查中断标志来确定激活是否成功,并返回相应的布尔值,这种明确的成功/失败反馈机制有助于上层逻辑做出正确的响应。
private IEnumerator ActivateAbility(ABC_IEntity Originator) {//Track what time we activated this abilitythis.abilityActivationTime = Time.time;//reset counter of how long ability prepared forthis.abilitySecondsPrepared = 0;//Let originator know that the ability is being activated for tracking purposesOriginator.AddToActivatingAbilities(this);//if enabled then raise the ability activation event to inform other scripts that have subscribedif (this.abilityActivationRaiseEvent == true)this.RaiseOriginatorsAbilityActivationEvent(Originator);//Reset any activation interruption flagsthis.abilityActivationInterrupted = false;//Reset any hitStops this.hitStopCurrentlyActive = false;this.hitStopCurrentTotalExtendDuration = 0;//Activate any linked abilities set to activate when this ability does this.ActivateLinkedAbilities(Originator);// define raycast position if travel type is crosshairif (this.travelType == TravelType.Crosshair) {rayCastTargetPosition = this.GetCrosshairRayCastPosition(Originator);}// get all the targets from originator for rest of code yield return ABC_Utilities.mbSurrogate.StartCoroutine(EstablishTargets(Originator));// Check to make sure ability wasn't interruptedif (this.AbilityActivationInterrupted() || this.RestrictedByHit(Originator)) {//Make sure animator is back to normal Originator.ModifyAnimatorSpeed(Time.time, 1);//Let Originator know that the ability is no longer being activatedOriginator.RemoveFromActivatingAbilities(this);yield break;}// start preparingyield return ABC_Utilities.mbSurrogate.StartCoroutine(this.StartPreparing(Originator));// start initiating phaseyield return ABC_Utilities.mbSurrogate.StartCoroutine(this.StartInitiating(Originator));//Initiate abilityyield return ABC_Utilities.mbSurrogate.StartCoroutine(this.InitiateAbility(Originator));
}
这个协程是整个技能系统的核心执行流程,它采用了状态机的设计思想来管理技能激活的复杂过程。协程开始时首先记录当前的游戏时间作为技能激活时间戳,这个时间戳将用于后续的冷却计算、持续时间判定等多个时间相关的逻辑计算。系统将当前技能添加到发起者的激活技能列表中,这样做的目的是为了支持多技能并发激活的情况,同时也便于系统在角色死亡或状态异常时统一清理所有激活中的技能。事件系统的触发是通过RaiseOriginatorsAbilityActivationEvent来实现的,这个事件机制允许其他系统(如UI更新、音效播放、特效触发等)响应技能的激活,实现了松耦合的系统架构。技能的联动激活功能通过ActivateLinkedAbilities来实现,这允许一个技能触发其他相关技能的激活,可以用来实现复杂的技能组合效果。目标建立过程通过EstablishTargets协程来完成,这个过程包括目标验证、距离检查、朝向调整等多个步骤,确保技能能够正确地锁定和命中目标。最后,技能的实际执行被分为三个阶段:准备阶段负责播放准备动画和特效,启动阶段处理技能的前置逻辑,执行阶段则创建投射物或直接应用效果,这种三阶段设计让技能的表现更加丰富和真实。
private bool TargetExists(ABC_IEntity Originator) {// if no target still travel is true and the activated variable is not none then this use to be a another travel type ability so we are going to swap it back before we continue with target validation if (this.noTargetStillTravel == true && this.noTargetStillTravelActivated != NoTargetStillTravelPreviousType.None) {this.travelType = (TravelType)System.Enum.Parse(typeof(TravelType), noTargetStillTravelActivated.ToString());this.noTargetStillTravelActivated = NoTargetStillTravelPreviousType.None;}// if we don't have a target and the traveltype is selected target but 'no target still travel' setting is true then we want to swap to traveltype forwardif (Originator.target == null && (this.auxiliarySoftTarget == false || this.auxiliarySoftTarget == true && Originator.softTarget == null)&& this.travelType == TravelType.SelectedTarget && this.noTargetStillTravel == true && this.abilityBeforeTarget == false) {// turn the flag to the current travel type to let rest of code know its been activated ready to be changed back next timethis.noTargetStillTravelActivated = (NoTargetStillTravelPreviousType)System.Enum.Parse(typeof(NoTargetStillTravelPreviousType), this.travelType.ToString());this.travelType = TravelType.Forward;Originator.AddToDiagnosticLog("Casting " + this.name + " with forward projection as no target has been selected");}// if starting position is on an object and it no longer exists then correct target doesn't exist so we return falseif (this.startingPosition == StartingPosition.OnObject && this.startingPositionOnObject.GameObject == null) {Originator.AddToDiagnosticLog("Can't spawn " + this.name + " OnObject as it doesn't exist");if (Originator.LogInformationAbout(LoggingType.AbilityActivationError))Originator.AddToAbilityLog("Can't spawn OnObject as it doesn't exist");return false;}return true;
}
实现了智能的目标适配机制,它的核心思想是在理想条件不满足时自动降级到可执行的替代方案,而不是简单地失败。当技能原本需要选定目标但当前没有目标时,系统会自动将技能的移动类型从"选定目标"切换为"向前发射",这样确保了技能仍然可以执行,只是改变了其行为模式。系统使用了状态记录机制来保存原始的移动类型设置,通过noTargetStillTravelActivated枚举来记录之前的状态,这样在下次激活时可以恢复到原始设定。这种设计特别适合动作游戏的快节奏战斗,玩家不会因为没有精确选择目标而导致技能完全无法使用。代码中还包含了详细的错误检查和日志记录,当起始位置对象不存在时会记录诊断信息并返回失败,这种详细的日志记录对于调试复杂的技能系统非常重要。整个函数采用了渐进式验证的策略,从最宽松的条件开始检查,逐步收紧要求,最终确保技能能够在各种情况下都有合理的行为表现。
public bool IsComboBlocked(ABC_IEntity Originator, bool IgnoreComboLockResets = false, bool AIActivated = false) {//Determine combo next activate leeway (if AI this is increased)float ComboNextActivateTimeLeeway = 0f;if (AIActivated == true)ComboNextActivateTimeLeeway = 4f;// ability has not been set up as a combo, is a scroll ability or is trigger by an input combo (different combo system F, F, B etc) so we can return true as it is allowed to activateif (this.abilityCombo == false || this.triggerType == TriggerType.InputCombo || this.scrollAbility == true) {// ability is not a combo so we have just broken any combos that we previously had so we can reset them all and return true if (IgnoreComboLockResets == false && AIActivated == false)ResetAllComboLocks(Originator);return false;}// get a temp list to collect all abilities that are grouped and chosen to be part of a combo List<ABC_Ability> tempComboAbilities = new List<ABC_Ability>();List<ABC_Ability> Abilities = Originator.CurrentAbilities;int positionInList = 0;for (int i = 0; i < Abilities.Count; i++) {// if the input of the ability is the same then add to temp list as long as it's enabled, not a scroll ability and its an ability combo and it has the same air/land typeif ((Abilities[i].key == this.key && this.keyInputType == InputType.Key || Abilities[i].keyButton == this.keyButton && this.keyInputType == InputType.Button)&& Abilities[i].scrollAbility == false && Abilities[i].abilityCombo == true && Abilities[i].isEnabled() == true && (Abilities[i].LandOrAir == AbilityLandOrAir.LandOrAir || Abilities[i].LandOrAir == this.LandOrAir)) {tempComboAbilities.Add(Abilities[i]);// if abilities compared is the same track what index it was (where in the group the ability is order wise)if (Abilities[i] == this) {positionInList = tempComboAbilities.Count - 1;}}}
}
实现了一个复杂而灵活的连击系统,它采用了动态分组和时间窗口的机制来管理技能的连击行为。系统首先为AI操作提供了特殊的时间宽容度,将连击窗口扩展到4秒,这是因为AI的反应和决策时间与人类玩家不同,需要更宽松的条件来确保AI能够正确执行连击。连击分组的逻辑是基于输入按键来实现的,所有使用相同按键的技能会被自动归类到同一个连击组中,系统通过遍历角色的所有技能来动态构建这个分组,这种设计的好处是无需预先定义连击组,而是根据技能配置自动形成。在分组过程中,系统会过滤掉不符合条件的技能,包括未启用的技能、滚动技能、非连击技能等,同时还会考虑技能的环境限制(如地面或空中),确保连击只在合适的情况下触发。位置索引的计算是通过遍历过程中的计数来实现的,当遍历到当前技能时,记录其在连击序列中的位置,这个位置信息将用于后续判断该技能是否应该在当前时刻激活。整个连击系统的设计理念是让玩家能够通过重复按同一个键来执行不同的技能动作,形成流畅的战斗组合,这种设计在动作游戏中非常常见且有效。
工作流程
整个能力系统工作流程分为5个主要阶段,从触发验证到最终清理,确保技能激活的可靠性和完整性。
阶段1:触发验证
技能触发请求通过Activate()方法进入系统,首先执行CanActivate()进行全面检查,包括技能状态、触发条件、目标要求、冷却时间等验证,失败则直接返回false。
阶段2:初始化准备
验证通过后停止武器格挡状态,启动ActivateAbility协程,记录激活时间戳,将技能添加到角色的激活技能列表,触发激活事件通知其他系统,同时激活所有关联技能。
阶段3:目标建立
通过EstablishTargets()方法建立技能目标,处理不同类型的目标定位(选定目标、十字准星、鼠标位置等),验证目标有效性和距离范围,失败则清理状态并退出。
阶段4:三阶段执行
- 准备阶段(StartPreparing):播放准备动画和特效,计算技能参数,处理移动限制
- 启动阶段(StartInitiating):面向目标,启动动画,激活武器轨迹等启动逻辑
- 执行阶段(InitiateAbility):创建投射物,应用效果,处理碰撞检测和伤害计算
阶段5:完成清理
技能执行完成后从激活列表中移除,清理临时数据和状态,触发完成事件,激活后续依赖的技能,确保系统状态一致性。
每个阶段都具备完善的中断检查机制,一旦检测到异常情况(角色被控制、目标消失、资源不足等)会立即停止执行并进行相应清理,保证系统的稳定性和响应性。
实体管理系统
实体管理系统是整个框架的基础核心,负责统一管理游戏中所有参与战斗的对象,包括玩家、敌人、NPC等。它通过ABC_IEntity接口为所有游戏对象提供标准化的属性访问和行为控制,确保技能系统、效果系统和其他模块能够以一致的方式操作不同类型的游戏对象。
功能实现
/// <summary>
/// Transform of the entity
/// </summary>
[NonSerialized]
protected Transform _entityTransform;/// <summary>
/// GameObject of the entity
/// </summary>
[NonSerialized]
protected GameObject _entityGameObj;/// <summary>
/// ABC Controller component attached to the entity (includes mana, logs and target information)
/// </summary>
[NonSerialized]
protected ABC_Controller _entityABC;/// <summary>
/// ABC Statemanger component attached to the entity (handles effect activation, health)
/// </summary>
[NonSerialized]
protected ABC_StateManager _entitySM;/// <summary>
/// The animator for the entity
/// </summary>
[NonSerialized]
protected Animator _ani;
实现了实体系统的组件缓存机制,通过[NonSerialized]标记确保这些引用不会被序列化,避免了潜在的序列化问题同时节省了内存。系统将所有常用的Unity组件(Transform、GameObject、Rigidbody、Animator等)以及ABC自定义组件(Controller、StateManager等)的引用缓存在实体对象中,这样做的好处是避免了频繁的GetComponent调用,大大提升了性能。每个实体在创建时会通过SetupEntity方法一次性获取所有需要的组件引用,后续的操作都直接使用缓存的引用,这种设计模式在高频调用的游戏系统中非常重要,能够有效减少CPU开销。
protected void SetupEntity(GameObject Obj, ABC_IEntity Entity) {// game object this._entityGameObj = Obj;//get the ABC Controller so we can retrieve settingsthis._entityABC = ABC_Utilities.TraverseObjectForComponent(Obj, typeof(ABC_Controller)) as ABC_Controller;//get the ABC State Manager so we can retrieve settingsthis._entitySM = ABC_Utilities.TraverseObjectForComponent(Obj, typeof(ABC_StateManager)) as ABC_StateManager;// set entity transform to save on processthis._entityTransform = this._entityGameObj.transform;// set rigidbody so we can save on processthis._entityRigidbody = ABC_Utilities.TraverseObjectForComponent(Obj, typeof(Rigidbody)) as Rigidbody;//Get Collider to save on processthis._entityCollider = ABC_Utilities.TraverseObjectForComponent(Obj, typeof(Collider)) as Collider;//Get any capsule Collider to save on processthis._entityCapsuleCollider = ABC_Utilities.TraverseObjectForComponent(Obj, typeof(CapsuleCollider)) as CapsuleCollider;// set Animator this._ani = ABC_Utilities.TraverseObjectForComponent(Obj, typeof(Animator)) as Animator;//get ABC movement controllerthis._abcMovementController = ABC_Utilities.TraverseObjectForComponent(Obj, typeof(ABC_MovementController)) as ABC_MovementController;//Create input Manager
#if ENABLE_INPUT_SYSTEMPlayerInput PI = ABC_Utilities.TraverseObjectForComponent(Obj, typeof(PlayerInput)) as PlayerInput;if (PI != null)this._entityInputManager = new ABC_InputManager(PI);else //if no player input on the entity then find a global onethis._entityInputManager = new ABC_InputManager();
#elsethis._entityInputManager = new ABC_InputManager();
#endif//Update entity property with the actual entity object created throughout the system this._entity = Entity;
}
实现了智能的组件发现和初始化机制,使用ABC_Utilities.TraverseObjectForComponent方法来递归搜索对象及其子对象以找到所需的组件,这种设计允许组件可以放置在对象层次结构的任何位置,提供了极大的灵活性。系统会自动检测不同类型的组件,如果某个组件不存在也不会报错,而是优雅地处理空引用情况。输入管理器的创建特别考虑了新旧输入系统的兼容性,通过编译指令#if ENABLE_INPUT_SYSTEM来适配不同的Unity版本和输入系统配置。整个初始化过程是非阻塞的,即使某些组件缺失也不会影响实体的基本功能,这种容错设计确保了系统的稳定性和可扩展性。
protected void AddToFunctionRequestTimeTracker(string Request, float LatestTime) {//if the key doesn't exist then add itif (functionRequestTimeTracker.ContainsKey(Request) == false) {functionRequestTimeTracker.Add(Request, LatestTime);} else {//else update the timefunctionRequestTimeTracker[Request] = LatestTime;}
}protected bool IsLatestFunctionRequestTime(string Request, float TimeToCheck) {//If we have not tracked this request before then this must be the latest if (functionRequestTimeTracker.ContainsKey(Request) == false)return true;//return if the time provided is the same as the latest request time tracked (therefore it is the latest call)return functionRequestTimeTracker[Request] == TimeToCheck;
}
这段代码实现了一个巧妙的函数调用冲突检测机制,通过记录每个函数类型的最新调用时间来防止重复或过时的函数调用产生冲突。当多个系统可能同时调用相同的功能(比如移动控制、动画播放等)时,这个机制确保只有最新的调用生效,避免了状态混乱和不可预期的行为。字典functionRequestTimeTracker使用函数名作为键,调用时间作为值,通过时间戳比较来判断当前调用是否为最新的有效调用。这种设计在复杂的游戏系统中非常重要,特别是当技能效果、状态变化、移动控制等多个系统可能同时影响同一个实体时,它确保了系统行为的一致性和可预测性。
protected void FreezeMovement(float FunctionRequestTime, bool FreezeMovement) {//If toggle movement has already been called by another part of the system, making this request time not the latest then return here to stop overlapping calls if (this.IsLatestFunctionRequestTime("FreezeMovement", FunctionRequestTime) == false)return;//Record new latest time this request was calledthis.AddToFunctionRequestTimeTracker("FreezeMovement", FunctionRequestTime);ABC_FreezePosition freezePosComponent = this.gameObject.GetComponent<ABC_FreezePosition>();//If we are not freezing movement then disable the component if it existsif (FreezeMovement == false && freezePosComponent != null)freezePosComponent.enabled = false;//Else if we are freezing movement then make sure the freeze position component is added and enabledif (FreezeMovement == true) {//If component hasn't already been added then add oneif (freezePosComponent == null)freezePosComponent = gameObject.AddComponent<ABC_FreezePosition>();freezePosComponent.enableFreezePosition = true;freezePosComponent.enabled = true; // else component already added so enable it}
}protected void StopMovement(float FunctionRequestTime, bool StopMovement) {//If toggle movement has already been called by another part of the system, making this request time not the latest then return here to stop overlapping calls if (this.IsLatestFunctionRequestTime("StopMovement", FunctionRequestTime) == false)return;//Record new latest time this request was calledthis.AddToFunctionRequestTimeTracker("StopMovement", FunctionRequestTime);//If entity was disabled during the call then we can end hereif (_entityGameObj.activeInHierarchy == false)return;ABC_DisableMovementComponents disableMovementComponents = this.gameObject.GetComponent<ABC_DisableMovementComponents>();//If we are not stopping movement then disable the component if it existsif (StopMovement == false && disableMovementComponents != null)disableMovementComponents.enabled = false;//If stopping movementif (StopMovement == true) {//If component hasn't already been added then add oneif (disableMovementComponents == null)disableMovementComponents = this.gameObject.AddComponent<ABC_DisableMovementComponents>();disableMovementComponents.haltRigidbody = true;disableMovementComponents.haltNavAgent = true;disableMovementComponents.disableCharacterController = true;disableMovementComponents.blockABCAINavigation = true;disableMovementComponents.enabled = true;}
}
这段代码展示了实体系统的动态组件管理能力,通过运行时添加和移除专用组件来控制实体的不同行为状态。FreezeMovement和StopMovement功能通过不同的机制来限制角色移动:FreezeMovement通过ABC_FreezePosition组件冻结角色的世界坐标位置,而StopMovement通过ABC_DisableMovementComponents组件禁用各种移动相关的组件。这种设计的优势是每种控制方式都有其特定的用途和效果,同时使用前面提到的时间追踪机制来防止调用冲突。系统会动态地添加所需的组件,当不再需要时又会禁用这些组件,这种按需组件管理的方式既保证了功能的完整性,又避免了不必要的性能开销。
[System.Serializable]
public partial class ABC_IEntity : ABC_IEntityBase {/// <summary>/// Constructor to make the entity object. Add other scripts here if required. /// </summary>/// <param name="Obj">object to be created into an entity object</param>/// <param name="StaticTracking">If true then the entity object will be added to a global static dictionary which can be retrieved later, stopping the need to make another IEntity for this gameobject</param>public ABC_IEntity(GameObject Obj, bool StaticTracking = true) : base(Obj, StaticTracking) {//Setup the Entity finding and storing all components to be referenced and used later on. this.SetupEntity(Obj, this);//If Object has any ABC scripts then Track the object in global Utilities code to be retrieved later for performance if (StaticTracking && (this._entitySM != null || this._entityABC != null))ABC_Utilities.AddStaticABCEntity(Obj, this);}
}
这段代码展示了实体系统的继承设计和全局追踪机制,ABC_IEntity类继承自ABC_IEntityBase并使用partial关键字,这种设计允许用户在不修改原始代码的情况下扩展实体功能。构造函数中的StaticTracking参数控制是否将实体对象添加到全局静态字典中,这个全局追踪系统的目的是避免为同一个GameObject重复创建IEntity对象,提高性能并确保数据一致性。只有当对象确实包含ABC相关组件时才会被添加到全局追踪中,这种条件性追踪既节省了内存又提高了查找效率。通过ABC_Utilities.AddStaticABCEntity方法,系统建立了一个高效的实体查找机制,其他系统可以快速根据GameObject获取对应的IEntity对象,避免了重复的组件查找和对象创建过程。
工作流程
实体管理系统采用5步初始化流程,从GameObject创建到最终可用,确保所有组件正确缓存和追踪。
构造初始化
当为GameObject创建ABC_IEntity时,系统首先检查是否启用静态追踪功能,这决定了实体是否会被添加到全局管理字典中。
组件扫描缓存
通过SetupEntity方法递归扫描对象及其子对象,自动发现并缓存所有相关组件引用,包括Unity内置组件和ABC自定义组件。
全局追踪注册
如果实体包含ABC_Controller或ABC_StateManager组件且启用了静态追踪,系统会将其注册到全局字典中,便于后续快速查找。
生命周期管理
实体系统提供完整的生命周期管理,包括组件状态控制、移动限制、属性调整、事件触发等功能,通过时间追踪机制避免操作冲突。
扩展性支持
通过partial类设计和virtual方法,系统支持用户自定义扩展,可以在不修改源码的情况下添加新功能或替换现有实现。
整个系统的设计核心是提供统一的实体抽象层,让技能系统、效果系统等其他模块能够以一致的方式操作不同类型的游戏对象,同时通过智能缓存和全局追踪机制确保高性能和数据一致性。
投射物系统
ABC投射物系统是技能表现的核心执行模块,负责管理所有飞行物体的创建、移动、碰撞检测和效果应用。它通过ABC_Projectile和ABC_ProjectileTravel两个核心组件,实现了从子弹、法术到复杂的追踪导弹等各种投射物行为,为技能系统提供了丰富的视觉表现和交互体验。
功能实现
/// <summary>
/// Property to hold the Ability object which has all the information regarding the ability including configured settings.
/// </summary>
public ABC_Ability ability = null;/// <summary>
/// The entity which has activated the ability also known as the Originator throughout ABC.
/// </summary>
public ABC_IEntity originator;/// <summary>
/// If one exists then the target of the Ability Projectile. Not always used it depends on the current travel type.
/// </summary>
public GameObject targetObj = null;/// <summary>
/// Records the time the projectile was activated.
/// </summary>
public float timeActivated;/// <summary>
/// Keeps track of all surrounding objects linked to this projectile. SurroundingObjects are GameObjects which have temporarily became apart of the Ability Projectile. Commonly used for telekinetic like effects.
/// </summary>
public List<GameObject> surroundingObjects;/// <summary>
/// Holds all Colliders of the projectile (Including SurroundingObjects).
/// </summary>
public Collider[] meColliders;
这段代码建立了投射物的完整状态管理架构,每个投射物都携带完整的技能信息引用,确保在整个生命周期中都能访问原始技能配置。系统通过originator和targetObj维护发起者和目标的关系,这对于复杂的技能逻辑(如反弹、追踪、友军识别等)至关重要。surroundingObjects列表实现了一个独特的功能,允许投射物在飞行过程中"吸收"环境中的物体,这可以用来实现念力技能、聚集效应等高级功能。碰撞器数组meColliders不仅包含投射物自身的碰撞器,还包括所有附属对象的碰撞器,这种设计为复杂形状的投射物和多部件投射物提供了完整的碰撞检测支持。
public void ActivateCollision(GameObject CollidedObj, CollisionType Type, Vector3 HitPoint = default(Vector3)) {// if the projectile script is not active then the collider manager should not be activatedif (this.enabled == false)return;originator.AddToDiagnosticLog(this.ability.name + " Collided with: " + CollidedObj.name);if (this.CorrectCollision(CollidedObj, Type) == false)return;originator.AddToDiagnosticLog(CollidedObj.name + " collision validated. Object meets criteria for collision with " + this.ability.name);//Determines if ability should be destroyedbool destroyAbility = false;// manages what happens with destory on impact switch (ability.impactDestroy) {case ImpactDestroy.DestroyOnAll:this.CollideHandler(CollidedObj, HitPoint);break;case ImpactDestroy.DestroyOnABCProjectiles:// if we just collided with another ABC Projectile if (this.FindProjectileObject(CollidedObj) != null)destroyAbility = true;this.CollideHandler(CollidedObj, HitPoint, destroyAbility, false);break;case ImpactDestroy.DestroyOnAllNotABCProjectile:// if we just collided with anything that isn't a projectileif (this.FindProjectileObject(CollidedObj) == null)destroyAbility = true;this.CollideHandler(CollidedObj, HitPoint, destroyAbility, false);break;}
}
这段代码实现了高度智能的碰撞检测和分类处理机制,通过多层验证确保只有符合条件的碰撞才会被处理。首先进行基础的活跃状态检查,然后通过CorrectCollision方法验证碰撞的有效性,这个方法会检查目标类型、标签匹配、团队关系等复杂条件。系统根据技能配置的impactDestroy设置来决定投射物的销毁行为,这种枚举驱动的设计让技能设计师能够精确控制投射物与不同类型对象碰撞时的行为。例如,某些技能可能只在撞击非投射物对象时销毁,而某些技能可能需要穿透其他投射物但在撞击实体时销毁,这种灵活的碰撞策略支持了各种复杂的技能机制。
private void CollideHandler(GameObject CollidedObj, Vector3 HitPoint = default(Vector3), bool Destroy = true, bool Bounce = true) {//Apply effects to the collided objectability.ApplyAbilityEffectsToObject(CollidedObj, this.originator, this.gameObject, HitPoint, false, false, this.abilitySecondsPrepared);//loop through all surrounding objects to apply effectsif (meColliders != null) {foreach (Collider meCol in meColliders.Where(c => c.transform.name.Contains("*_ABCSurroundingObject"))) {// if set to apply if (ability.projectileAffectSurroundingObject == true)ability.ApplyAbilityEffectsToObject(meCol.gameObject, this.originator, this.gameObject, default(Vector3), false, false, this.abilitySecondsPrepared);}}// Start bounce handler if we can bounceif (Bounce && this.CanBounce(CollidedObj)) {StartCoroutine(BounceHandler(CollidedObj));} else {//Attach object on impact if probability is metif (ability.attachToObjectOnImpact == true && this.gameObject.transform.parent == null && ABC_Utilities.DiceRoll(ability.attachToObjectProbabilityMinValue, ability.attachToObjectProbabilityMaxValue) == true) {//stop gravityif (ability.applyGravity == true)this.DisableGravity();//Stop travellingthis.ToggleProjectileTravel(false);//Find the nearest boneTransform closestBone = ABC_Utilities.GetClosestBoneFromPosition(ABC_Utilities.GetStaticABCEntity(CollidedObj), HitPoint);//Parent to closest bone if foundif (closestBone != null && ability.attachToObjectNearestBone == true) {//Start from closest bonethis.gameObject.transform.position = closestBone.transform.position;this.gameObject.transform.parent = closestBone.transform;//Move from origin towards hit locationVector3 movetowards = Vector3.MoveTowards(this.gameObject.transform.position, HitPoint, 0.1f * CollidedObj.transform.localScale.x);this.gameObject.transform.position = new Vector3(movetowards.x, HitPoint.y, movetowards.z);this.gameObject.transform.Translate(0, 0, -ability.attachToObjectStickOutFactor, Space.Self);}}}
}
这段代码展示了投射物碰撞后的综合处理流程,它不仅仅是简单的效果应用,而是一个完整的交互系统。首先对主要碰撞目标应用技能效果,然后遍历所有附属的周围对象也应用相应效果,这种设计支持了群体攻击和复合效果的实现。反弹机制通过CanBounce和BounceHandler来实现,为技能增加了更丰富的物理表现。特别值得注意的是投射物附着机制,当概率条件满足时,投射物可以附着到目标上,系统会智能地寻找最近的骨骼节点进行附着,这对于箭矢、标枪等武器的真实表现非常重要。附着过程还考虑了缩放因子和插入深度,确保视觉效果的真实性。
/// <summary>
/// Moves the object by adding force/velocity to the objects forward transform
/// </summary>
private void Move() {//If in hit stop then slow movement right downif (meABCProjectile != null && meABCProjectile.ability.hitStopCurrentlyActive == true) {meRigidbody.velocity = transform.forward * 0.3f;return;}// move out from caster:if (this.travelPhysics.ToUpper() == "VELOCITY")meRigidbody.velocity = transform.forward * this.travelSpeed;if (this.travelPhysics.ToUpper() == "FORCE")meRigidbody.AddForce(transform.forward * this.travelSpeed);
}/// <summary>
/// Function called by travel types where the projectile will move towards a target/position.
/// </summary>
private void TravelToDestination() {//If travel is not enabled we can end hereif (this.travelEnabled == false)return;// record destination positionthis.destination = this.GetDestinationPosition();//If we have reached the destination and we are always moving towards the target then set the projectile object as a child of the target so it can just move with the target.if (Vector3.Distance(meTransform.position, this.destination) < 0.4 && this.continuouslyTurnToDestination == true) {// we have reached target already unless they suddenly move very fast out of distance then we need to stay at target (MMO RPG style)meRigidbody.velocity = Vector3.zero;meTransform.position = this.destination;// If we are in boomerange mode and we have reached back to originator then we can destroy now if (this.boomerangEnabled == true && this.targetObject == this.startingPositionObject && meABCProjectile != null)meABCProjectile.Destroy();// make sure we only set the parent once and only if a target is present and it's not the main world if (this.parentSet == false && this.targetObject != null && this.travelType != TravelType.ToWorld) {meTransform.parent = targetObject.transform;this.parentSet = true;}return;}// If we are currently seeking a target then look at destination (If not seeking target projectile will just move forward in its starting direction)if (this.seekTarget == true)TurnToDestination();// move object this.Move();
}
这段代码实现了高度灵活的投射物移动控制系统,支持多种物理模式和移动策略。系统首先检查是否处于命中停顿状态,如果是则大幅降低移动速度来实现时间暂停效果,这种细节处理增强了战斗的戏剧性。移动物理系统支持两种模式:速度模式直接设置刚体速度实现平滑移动,力模式通过添加力来实现更真实的物理表现。目标导向移动通过TravelToDestination方法实现,它会动态计算目标位置并调整朝向,支持连续追踪和一次性瞄准两种模式。当投射物到达目标附近时,系统会自动将其设为目标的子对象,这样可以实现"附着"效果,让投射物跟随目标移动。回旋镖模式的实现也在这里体现,当投射物返回到发起者位置时会自动销毁,这种特殊移动模式为某些技能提供了独特的行为表现。
void OnEnable() {//reset variablesthis.boomerangEnabled = false;this.parentSet = false;this.enableTurnToDestination = true;this.seekTarget = false;this.travelEnabled = false;// reset target position (forgets previous)this.destination = Vector3.zero;// cache root of transform and rigidbody meTransform = transform.root;meRigidbody = transform.GetComponent<Rigidbody>();// get projectile script if (gameObject.GetComponent<ABC_Projectile>() != null)meABCProjectile = gameObject.GetComponent<ABC_Projectile>();//Starts travel after the time given. Invoke("EnableTravel", this.travelDelay);// setting to stop projectile after it has travelled x time. if (applyTravelDuration == true && (this.travelDurationOriginatorTagsRequired.Count == 0 || ABC_Utilities.ObjectHasTag(originator, this.travelDurationOriginatorTagsRequired)))Invoke("DisableTravel", this.travelDurationTime + this.travelDelay);// if were doing boomerang then enable after delayif (boomerangMode == true)Invoke("ActivateBoomerangMode", this.boomerangDelay);// in a small time we will go towards targetInvoke("EnableSeekTarget", this.seekTargetDelay + this.travelDelay);// stop any previous velocity GetComponent<Rigidbody>().velocity = Vector3.zero;
}
这段代码展示了投射物的智能初始化系统,它确保每次启用时都从一个干净的状态开始。系统会重置所有状态标志,清除之前的目标信息,并重新缓存组件引用,这种彻底的重置机制确保了对象池中的投射物能够正确复用。通过一系列的Invoke调用,系统实现了复杂的时序控制,不同的功能会在不同的时间点激活,例如移动延迟、追踪延迟、回旋镖模式延迟等。移动持续时间的控制还考虑了发起者的标签条件,只有满足特定标签要求的实体才会应用持续时间限制,这种条件性控制为不同类型的角色提供了差异化的技能表现。最后清零初始速度确保投射物从静止状态开始移动,避免了之前状态的干扰。
工作流程
创建初始化
投射物由技能系统从对象池中获取或新建,立即进行组件缓存、状态重置、参数配置等初始化工作,确保干净的起始状态。
延迟控制启动
通过多层延迟机制控制投射物行为时序,包括移动延迟、追踪延迟、回旋镖延迟等,不同功能在预设时间点激活。
智能移动控制
根据移动类型执行不同的移动策略,支持直线前进、目标追踪、回旋镖等模式,同时处理物理应用、命中停顿、重力影响等状态。
多层碰撞检测
通过碰撞器检测与环境和对象的接触,验证碰撞有效性,根据销毁策略决定是否处理碰撞,支持复杂的碰撞过滤规则。
效果应用处理
对碰撞目标和附属对象应用技能效果,包括伤害计算、状态应用、视觉特效等,实现技能的实际游戏效果。
状态分支处理
根据技能配置决定后续行为:反弹则改变方向继续移动,附着则停止移动并跟随目标,销毁则进入清理流程。
销毁回收管理
播放销毁特效、处理溅射伤害、清理状态数据、归还对象池等清理工作,确保资源正确回收和系统性能。
控制层 (Control Layer)
移动控制系统
ABC移动控制系统是整个角色控制的核心模块,负责处理玩家和AI的所有移动行为,包括基础移动、跳跃、跑酷、锁定目标等复杂功能。它通过Unity的CharacterController组件实现物理移动,并集成了丰富的动画、音效和特效系统
功能实现
/// <summary>
/// Determines how fast the entity will move
/// </summary>
public float moveForce = 5f;/// <summary>
/// If enabled then movement will use acceleration
/// </summary>
public bool useAcceleratedMovement = true;/// <summary>
/// Amount of acceleration applied to movement
/// </summary>
public float moveAcceleration = 4f;/// <summary>
/// If true then once max acceleration has been hit a new animation will be triggered
/// </summary>
public bool enableMaxAcceleratedAnimation = true;/// <summary>
/// How long after max acceleration has been hit the accelerated animation will start
/// </summary>
public float intervalFromMaxAccelerationToAnimation = 2.5f;/// <summary>
/// If key is pressed then the entity will be switched to walking mode
/// </summary>
public KeyCode walkToggleKey = KeyCode.Comma;/// <summary>
/// If button is pressed then the entity will be switched to walking mode
/// </summary>
public string walkToggleButton = "Walk";/// <summary>
/// Determines how fast the entity will move when sprinting
/// </summary>
public float sprintForce = 7f;/// <summary>
/// Axis name of sprint button
/// </summary>
public string sprintButton = "Sprint";/// <summary>
/// Key input for sprinting
/// </summary>
public KeyCode sprintKey = KeyCode.LeftShift;
定义了移动系统的基础配置架构,通过分层的移动模式(走路、普通移动、冲刺)和加速度控制实现流畅的移动感受。系统支持键盘和手柄双输入方式,moveForce作为基础移动速度,sprintForce提供高速移动能力,加速系统通过useAcceleratedMovement和moveAcceleration参数控制移动的渐变效果,避免突兀的速度切换。
private void RotateAndMoveEntity() {//Call lock off handler StartCoroutine(this.TempLockOffHandler());// If crosshair movement key is held down prioritise the rotate to camera direction// else rotate the entity to face the direction we want to travel if we have not got a lock on target// else turn the entity to face lock on targetif (this.AdvancedRotationAllowed() == true && allowMovement == true) {//If entity does a sharp 180 turn then play rotation clip (gets current forward and dot of the next move direction (-1 is 180 degree turn)if (Vector3.Dot(this.transform.forward.normalized, this.moveDirection.normalized) <= -0.8f) {//If moving do accelerated 180 (unless already doing advanced 180)if (Time.time - this.LastMovementTime < 0.2f && ABCEntity.animationRunner.IsAnimationRunning(this.advancedRotation180Clip.AnimationClip) == false) {//Turned accelerated 180if (this.acceleratedRotationClip.AnimationClip != null) {StartCoroutine(ActivateAdvancedRotation(this.acceleratedRotationClip.AnimationClip, this.acceleratedRotationClipSpeed, this.acceleratedRotationStopPercentage, this.acceleratedRotationClipMask.AvatarMask));}} else { // else normal 180//Turned 180if (this.advancedRotation180Clip.AnimationClip != null) {StartCoroutine(ActivateAdvancedRotation(this.advancedRotation180Clip.AnimationClip, this.advancedRotation180ClipSpeed, this.advancedRotation180StopPercentage, this.advancedRotation180ClipMask.AvatarMask));}}} else if (Vector3.Dot(this.transform.right.normalized, this.moveDirection.normalized) >= 0.85f && Time.time - this.LastMovementTime > 0.1f) { // else if turning right//Turned Rightif (this.advancedRotationRightClip.AnimationClip != null) {StartCoroutine(ActivateAdvancedRotation(this.advancedRotationRightClip.AnimationClip, this.advancedRotationRightClipSpeed, advancedRotationRightStopPercentage, this.advancedRotationRightClipMask.AvatarMask));}} else if (Vector3.Dot((-this.transform.right).normalized, this.moveDirection.normalized) >= 0.85f && Time.time - this.LastMovementTime > 0.1f) {// else if turning left //Turned Leftif (this.advancedRotationLeftClip.AnimationClip != null) {StartCoroutine(ActivateAdvancedRotation(this.advancedRotationLeftClip.AnimationClip, this.advancedRotationLeftClipSpeed, this.advancedRotationLeftStopPercentage, this.advancedRotationLeftClipMask.AvatarMask));}}}
这段核心代码实现了智能旋转系统,通过向量点积计算角色当前朝向与目标移动方向的夹角关系。当夹角达到特定阈值时(180度转向、左右转向),系统会自动播放相应的旋转动画来增强视觉效果。TempLockOffHandler()提供临时解除锁定的功能,Vector3.Dot计算用于精确判断转向角度,不同的转向类型对应不同的动画剪辑和播放参数,实现了流畅自然的角色转向效果。
void Update() {//Check parkour handler, if parkour activates end update hereif (this.ParkourHandler())return; //Execute double jump handlerthis.DoubleJumpHandler();//Execute the jump handlerthis.JumpHandler();//Determine input and directionthis.DetermineMoveDirection();//Rotate and move entitythis.RotateAndMoveEntity();//Run animationsthis.Animate();}
移动系统的主循环控制代码,按照优先级顺序处理各种移动行为。首先检查跑酷系统,如果跑酷激活则跳过其他所有移动处理,确保跑酷动画的完整性。然后依次处理二段跳、普通跳跃、输入方向计算、旋转移动和动画播放。
工作流程
输入检测阶段
系统持续监听键盘、手柄等输入设备,收集移动、跳跃、冲刺、锁定等各种操作指令,同时检查当前移动权限状态。
跑酷优先处理
通过ParkourHandler()检测前方障碍物和跑酷条件,如果跑酷激活则立即执行跑酷动画并跳过其他移动处理。
跳跃系统处理
依次执行二段跳检查和普通跳跃逻辑,处理跳跃输入、重力应用、着地检测等跳跃相关状态管理。
移动方向计算
根据输入和相机方向计算最终移动向量,考虑锁定目标、十字准星模式、FPS模式等不同控制方式的影响。
旋转移动执行
智能旋转系统分析转向角度并播放相应动画,然后通过CharacterController执行实际的物理移动。
动画状态同步
根据当前移动状态、速度、方向等参数更新Animator参数,播放对应的移动、待机、跳跃等动画。
音效特效处理
在移动过程中触发脚步声、粒子特效等视听反馈,根据地面材质播放不同的音效,增强沉浸感。
动画系统
ABC动画系统是一个双层架构的高级动画控制框架,由ABC_AnimationsRunner和ABC_IKController两个核心组件构成。它不依赖传统的Animator State Machine,而是通过Unity的Playable Graph API直接控制动画播放,同时集成IK系统实现精确的骨骼控制。该系统为技能动画、移动动画和交互动画提供了统一的播放管理和无缝混合能力。
功能实现
public void PlayAnimation(AnimationClip Animation, float Delay = 0f, float Speed = 1f, float Duration = 0f, AvatarMask AvatarMask = null, bool InterruptCurrentAnimation = true, bool SkipBlending = false) {//If set to not interrupt any current animations and one is playing then return hereif (InterruptCurrentAnimation == false && this.playableGraph.IsValid() && this.playableGraph.IsPlaying()) {return;}//If any run and stop animations are currently in progress then stop them ready for the new oneif (this.aniRunStopCoroutines.Count > 0) {this.aniRunStopCoroutines.ForEach(c => StopCoroutine(c));this.aniRunStopCoroutines.Clear();}//Track the animation run and stop method which is about to activateIEnumerator newCoroutine = null;//Run animation and then stop it after duration StartCoroutine((newCoroutine = RunAndStopAnimation(Animation, Delay, Speed, Duration, AvatarMask, SkipBlending)));//Add the method to the tracker to remove early if required (another animation may start early) this.aniRunStopCoroutines.Add(newCoroutine);
}
这段代码实现了动画播放的入口控制逻辑,通过InterruptCurrentAnimation参数决定是否中断当前播放的动画,避免动画冲突。系统维护了三个协程列表(aniRunCoroutines、aniStopCoroutines、aniRunStopCoroutines)来管理不同类型的动画操作,确保在新动画开始前正确清理旧的协程,防止内存泄漏和状态混乱。RunAndStopAnimation协程负责完整的动画生命周期管理,从启动到自动停止。
//Smooth the weight to transition the IK to the right position
if (this.rightHandWeightState < this.rightHandWeight) {this.rightHandElapsedTime += Time.deltaTime;this.rightHandWeightState = Mathf.Lerp(0, 1, this.rightHandElapsedTime / this.rightHandTransitionSpeed);
} else {this.rightHandWeightState = this.rightHandWeight;this.rightHandElapsedTime = 0;
}// set right hand to target if it exists
if (this.rightHandTarget != null) this.SetIKToTarget(AvatarIKGoal.RightHand, this.rightHandTarget, this.rightHandWeightState);
elsethis.RemoveIK(AvatarIKGoal.RightHand);//Smooth the weight to transition the IK to the right position
if (this.leftHandWeightState < this.leftHandWeight) {this.leftHandElapsedTime += Time.deltaTime;this.leftHandWeightState = Mathf.Lerp(0, 1, this.leftHandElapsedTime / this.leftHandTransitionSpeed);
} else {this.leftHandWeightState = this.leftHandWeight;this.leftHandElapsedTime = 0;
}// set left hand to target if it exists
if (this.leftHandTarget != null)this.SetIKToTarget(AvatarIKGoal.LeftHand, this.leftHandTarget, this.leftHandWeightState);
elsethis.RemoveIK(AvatarIKGoal.LeftHand);
这段代码在OnAnimatorIK回调中实现了IK权重的平滑过渡控制,通过Mathf.Lerp函数和时间追踪确保IK骨骼不会突然跳跃到目标位置,而是按照设定的过渡速度平滑移动。每个IK目标都有独立的权重状态、经过时间和过渡速度参数,允许精确控制不同骨骼的IK强度和过渡效果,实现自然的手部或其他骨骼的目标跟踪。
//Create the playable graph and set time update mode
this.playableGraph = PlayableGraph.Create();
this.playableGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);// create and wrap the animation clip in a playable
clipPlayable = AnimationClipPlayable.Create(playableGraph, Animation);//Set the properties of the animation (speed, footOK)
clipPlayable.SetSpeed(Speed);
clipPlayable.SetApplyFootIK(true);//Create the animator animation playable output
var playableOutput = AnimationPlayableOutput.Create(this.playableGraph, "ABC_Animation", this.meAni);//Populate the global variable tracking the animation currently running in the Animator/Animation Runner
yield return StartCoroutine(this.GetCurrentAnimatorAnimation(CurrentRunningAnimation, CurrentRunningAvatarMask));//Create the high level mixer which will blend from one clip to another
this.mixerPlayable = AnimationMixerPlayable.Create(playableGraph, 2);
这段代码展示了Playable Graph的核心构建过程,首先创建播放图并设置为游戏时间更新模式,然后将动画剪辑包装成AnimationClipPlayable节点并配置播放属性。系统创建AnimationPlayableOutput作为最终输出连接到Animator组件,再通过AnimationMixerPlayable实现多个动画之间的混合功能,这种图形化的节点结构提供了比传统Animator State Machine更灵活和精确的动画控制能力。
工作流程
动画请求验证
接收来自技能系统、移动系统的动画播放请求,检查动画剪辑有效性、权限验证和播放参数合法性。
冲突检查处理
根据InterruptCurrentAnimation参数决定是否中断当前动画,检查Playable Graph状态,避免动画播放冲突。
协程生命周期管理
清理正在运行的动画协程,防止重复播放和内存泄漏,维护三个协程列表确保状态一致性。
Playable Graph构建
创建新的播放图,停止旧图并加入清理队列,建立AnimationClipPlayable和混合器节点的层次结构。
混合器配置设置
配置AnimationMixerPlayable参数,设置混合权重和过渡时间,处理AvatarMask分层动画需求。
动画播放执行
启动Playable Graph播放,应用动画速度、延迟和持续时间参数,开始实际的动画数据流处理。
IK系统集成处理
在OnAnimatorIK回调中处理IK目标设定,实现平滑的权重过渡,确保骨骼位置和旋转的精确控制。
进度追踪监控
持续监控动画播放进度,更新clipProgress属性,处理动画事件和状态同步,支持外部系统的进度查询。
清理回收管理
动画播放完成或被中断时,正确销毁Playable Graph,清理资源引用,重置状态变量,确保系统可以正常处理下一个动画请求。
相机系统
ABC相机系统是控制层的核心视觉控制模块,采用双层架构设计:ABC_CameraBase提供基础跟随和旋转功能,ABC_Camera提供高级距离管理和碰撞检测。该系统支持第三人称、第一人称、锁定目标等多种视角模式,与技能系统深度集成,为玩家提供流畅的视觉体验和精确的瞄准控制。
功能实现
/// <summary>
/// Handles and calculates the desired distance depending on user input and max and min settings
/// </summary>
private void DesiredDistanceHandler() {//If user scrolls the mouse wheel then change the desired distance if (this.inputManager.GetYAxis("Mouse ScrollWheel") > 0)desiredDistance -= 1;if (this.inputManager.GetYAxis("Mouse ScrollWheel") < 0)desiredDistance += 1;//Clamp the desired distance so it never goes over the max or min camera distancedesiredDistance = Mathf.Clamp(this.desiredDistance, minCameraDistance, maxCameraDistance);}
这个方法处理用户的滚轮输入来动态调整相机距离。当用户向前滚动滚轮时,期望距离减少1(拉近视角),向后滚动时增加1(拉远视角)。最后使用Mathf.Clamp确保距离始终在预设的最小和最大范围内,提供可控的缩放体验。
/// <summary>
/// Will set the current camera distance depending on situations like wall collision
/// </summary>
private void SetCurrentDistance() {//Set the desired camera positionVector3 desiredCameraPos = transform.parent.TransformPoint(cameraDirection * desiredDistance);RaycastHit hit;//Line cast to check for collisionif (Physics.Linecast(transform.parent.position, desiredCameraPos, out hit)) {//If we are not ignoring the object we just collided then change distanceif (this.IgnoreCollisionTags.Contains(hit.transform.tag) == false && hit.transform.name.Contains("ABC*_") == false) {currentDistance = (hit.distance * 0.57f); // collision has occured so move camera to the hit distance return; //return here as we don't want to clamp } else {currentDistance = this.desiredDistance; // else no collision so set to desired}} else {currentDistance = this.desiredDistance; // no collision so current distance can be set at the desired distance}//Clamp the current distance so its within the min and max settingcurrentDistance = Mathf.Clamp(this.currentDistance, minCameraDistance, maxCameraDistance);}
这是相机系统的核心智能功能。系统从跟随目标位置向期望相机位置发射射线检测障碍物。如果碰撞的对象不在忽略列表中(通过标签和名称过滤),则将当前距离设置为碰撞距离的57%,这个比例经过优化,既避免相机穿透墙壁,又保持舒适的视觉距离。支持标签过滤机制,可以忽略UI元素、特效等特定对象。
private void RotateCamera() {//If rotation is not enabled end hereif (this.enableRotation == false || this.enableRotation == true && this.rotationOnMouseHold == true && this.inputManager.GetKey(KeyCode.Mouse0) == false && this.inputManager.GetKey(KeyCode.Mouse1) == false)return;//If y rotation is blocked then ABC is probably in ability before target mode (stops up and down movement)if (this.enableYRotation == true) {float yAxis = ((this.IsInputAvailable("RightStickVertical") ? this.inputManager.GetYAxis("RightStickVertical") : 0) + this.inputManager.GetYAxis("Mouse Y") * this.inputSensitivity);//If set to invert the y axis then turn the positive to a negative or vice versaif (this.invertYAxis == false)yAxis = yAxis * -1;this.currentYRot += yAxis;}float xAxis = 0f;xAxis = ((this.IsInputAvailable("RightStickHorizontal") ? this.inputManager.GetXAxis("RightStickHorizontal") : 0) + this.inputManager.GetXAxis("Mouse X")) * this.inputSensitivity;this.currentXRot += xAxis;float clamp = this.clampAngle;if (this.InZoomMode() == true)clamp = this.zoomClampAngle;//Apply the angle clamp on the X axisthis.currentYRot = Mathf.Clamp(currentYRot, -clamp, clamp);Quaternion localRotation = Quaternion.Euler(this.currentYRot, this.currentXRot, 0f);transform.rotation = localRotation;}
这个方法实现了灵活的多输入设备旋转控制。首先检查旋转是否启用以及是否需要按住鼠标才能旋转(MMO模式)。然后分别处理Y轴(上下看)和X轴(左右看)的输入,支持鼠标和手柄输入的组合。系统支持Y轴反转设置,并根据是否处于缩放模式应用不同的角度限制,最后通过四元数应用旋转。
/// <summary>
/// Will handle what happens when ABC toggles ability before target on/off
/// </summary>
/// <param name="AbilityID">ID of ability that completed activating</param>
/// <param name="Enabled">True to enable X Rotation else false to disable</param>
public void ABCAbilityBeforeTargetToggleHandler(int AbilityID, bool Enabled) {//If ability before target is enabled then stop x rotationif (Enabled == true) {this.enableYRotation = false;if (this.hideLockMouse)Cursor.lockState = CursorLockMode.Confined;} else {this.enableYRotation = true;if (this.hideLockMouse)Cursor.lockState = CursorLockMode.Locked;}}
这个方法展示了相机系统与ABC技能系统的深度集成。当技能系统进入"瞄准前选择目标"模式时,相机会禁用Y轴旋转(上下看)并将鼠标锁定状态改为Confined(限制在屏幕内),确保玩家可以精确选择目标。当退出该模式时,恢复正常的Y轴旋转和鼠标锁定状态,实现无缝的技能瞄准体验。
工作流程
输入监听处理
系统监听鼠标滚轮、移动输入、旋转输入等多种用户操作,通过统一的输入管理器处理不同平台的输入设备,支持键盘鼠标和手柄的无缝切换。
动态距离计算
根据用户滚轮输入实时调整期望距离,向前滚动减少距离(拉近),向后滚动增加距离(拉远),并通过Clamp限制在合理范围内。
智能碰撞检测
使用射线检测从跟随目标到期望位置之间的障碍物,支持标签过滤系统,避免UI元素、特效等特定对象影响相机位置。
安全位置调整
当检测到碰撞时,计算碰撞距离的57%作为安全距离,这个比例既避免穿透墙壁又保持舒适视距,无碰撞时使用期望距离。
平滑移动插值
使用Vector3.Lerp实现相机位置的平滑过渡,避免突兀的位置跳跃,提供流畅的视觉体验。
多模式旋转跟随
支持自由旋转、锁定目标、FPS模式等不同视角控制方式,根据游戏状态和用户设置自动切换控制策略。
ABC系统深度集成
与技能系统集成处理瞄准辅助、目标选择、射线投射等功能,在技能瞄准时提供精确的视觉反馈和控制支持。
交互层 (Interaction Layer)
输入管理系统
ABC输入管理系统是交互层的核心模块,采用双系统兼容设计,同时支持Unity的新旧输入系统。它通过ABC_InputManager提供统一的输入接口,集成了虚拟鼠标系统以支持手柄操作,实现了跨平台、多设备的无缝输入体验。该系统为技能触发、移动控制、UI交互等所有游戏功能提供了标准化的输入处理能力。
功能实现
/// <summary>
/// ABC Input Manager which will integrate with both new and old Unity input systems.
/// </summary>
[System.Serializable]
public class ABC_InputManager {/// <summary>/// Instantiates New InputManager/// </summary>public ABC_InputManager() {//Legacy Input}#if ENABLE_INPUT_SYSTEM/// <summary>/// Instantiates New InputManager/// </summary>/// <param name="playerInput">player input for the entity</param>public ABC_InputManager(PlayerInput playerInput) {//set player input_playerInput = playerInput;}/// <summary>/// Stores player input component/// </summary>private PlayerInput _playerInput; /// <summary>/// Get/Set Method to retrieve and return player input component/// </summary>private PlayerInput playerInput {get {if (_playerInput == null) {//try and find input systemPlayerInput inputSystem = UnityEngine.Object.FindObjectsOfType<PlayerInput>().FirstOrDefault();//If found set variable and returnif (inputSystem != null)_playerInput = inputSystem;elseDebug.LogWarning("Player Input enabled and not found. Please add one to the scene manually or through the ABC character creation.");//If using gamepad then find virtual mouseif (this._virtualMouse == null && InputSystem.devices.Where(d => d is Gamepad).Count() > 0) {if (UnityEngine.Object.FindObjectsOfType<ABC_VirtualMouse>().Count() > 0) {this._virtualMouse = UnityEngine.Object.FindObjectsOfType<ABC_VirtualMouse>().First();} else {Debug.Log("Gamepad Detected, loading Virtual Mouse.");this._virtualMouse = UnityEngine.Object.Instantiate(Resources.Load("ABC-InputActions/VirtualMouse/VirtualMouse") as GameObject).GetComponentInChildren<ABC_VirtualMouse>();}}}return _playerInput; }}
这个构造函数和属性实现了双系统兼容的核心机制。通过预编译指令#if ENABLE_INPUT_SYSTEM判断当前使用的输入系统版本,新系统使用PlayerInput组件,传统系统使用默认构造函数。playerInput属性实现了智能的组件查找和虚拟鼠标的自动创建,当检测到手柄设备时会自动实例化虚拟鼠标系统。
/// <summary>
/// Will return the X value of the axis provided
/// </summary>
/// <param name="Axis">Axis to return the X value of</param>
/// <returns>value of the Axis provided</returns>
public float GetXAxis(string Axis) {
#if ENABLE_INPUT_SYSTEMif (IsButtonSupported(Axis) == true)return playerInput.actions.FindAction(Axis).ReadValue<Vector2>().x;switch (Axis) {case "X":case "Mouse X":if (Mouse.current == null)return 0f;return Mouse.current.delta.ReadValue().x;case "RightStickHorizontal": if (Gamepad.current == null)return 0f;return Gamepad.current.rightStick.ReadValue().x;case "Horizontal":if (IsButtonSupported("Move") == false)return 0;return playerInput.actions.FindAction("Move").ReadValue<Vector2>().x;default:return 0f;}#elsereturn Input.GetAxis(Axis);
#endif
}
轴值获取系统通过switch语句针对不同的轴类型进行特殊处理。对于新输入系统,首先检查是否为支持的按钮,然后根据轴名称分别处理鼠标移动、手柄摇杆、键盘方向等。这种设计确保了不同输入设备的一致性,同时保持了向后兼容性。
/// <summary>
/// The virtual mouse device that the component feeds with input.
/// </summary>
/// <value>Instance of virtual mouse or <c>null</c>.</value>
/// <remarks>
/// This is only initialized after the component has been enabled for the first time. Note that
/// when subsequently disabling the component, the property will continue to return the mouse device
/// but the device will not be added to the system while the component is not enabled.
/// </remarks>
private Mouse virtualMouse => m_VirtualMouse;[Space(10)]
[Header("Cursor")]
private CursorMode m_CursorMode = CursorMode.SoftwareCursor;[Tooltip("The graphic that represents the software cursor. This is hidden if a hardware cursor (see 'Cursor Mode') is used.")]
[SerializeField] private Graphic m_CursorGraphic;
[Tooltip("The transform for the software cursor. Will only be set if a software cursor is used (see 'Cursor Mode'). Moving the cursor "+ "updates the anchored position of the transform.")]
[SerializeField] private RectTransform m_CursorTransform;[Header("Motion")]
[Tooltip("Speed in pixels per second with which to move the cursor. Scaled by the input from 'Stick Action'.")]
[SerializeField] private float m_CursorSpeed = 400;
[Tooltip("Scale factor to apply to 'Scroll Wheel Action' when setting the mouse 'scrollWheel' control.")]
[SerializeField] private float m_ScrollSpeed = 45;
虚拟鼠标系统是ABC输入管理的一大特色功能。它通过CursorMode枚举支持软件光标和硬件光标两种模式,m_CursorSpeed控制光标移动速度,m_ScrollSpeed控制滚轮缩放。这个系统为手柄用户提供了完整的鼠标指针功能,解决了手柄操作UI的难题。
/// <summary>
/// Will return if any key(code) is being pressed, checking both new and old Unity Input Systems
/// </summary>
/// <returns>True if Key pressed, else false</returns>
public bool AnyKey() {
#if ENABLE_INPUT_SYSTEM//Check keyboard firstif (Keyboard.current.anyKey.wasPressedThisFrame == true) return true;//If no player input then return falseif (this.playerInput == null)return false; //If not check for other devicesforeach (InputAction input in this.playerInput.actions) {//If input is not pressed, contains UI or any looking mechanics then this would not be classed as a button pressif (input.IsPressed() == false || input.ToString().Contains("UI") || input.ToString().Contains("Mouse/delta") || input.ToString().Contains("rightStick"))continue;//If we got this far then the right button was pressed so return truereturn true; }//If we got this far then nothing been pressed so return falsereturn false; #elsereturn Input.anyKey;
#endif
}
AnyKey方法实现了智能的按键检测机制。对于新输入系统,首先检查键盘是否有按键按下,然后遍历所有InputAction,过滤掉UI相关、鼠标移动、右摇杆等非按键输入,确保只检测真正的按键操作。这种设计避免了误触发,提高了输入检测的准确性。
工作流程
输入检测处理
系统首先检测当前可用的输入设备,包括键盘、鼠标、手柄等,并根据设备类型选择相应的处理策略。
系统判断机制
通过预编译指令#if ENABLE_INPUT_SYSTEM判断当前使用的是Unity的新输入系统还是传统输入系统,确保向后兼容性。
PlayerInput处理
当使用新输入系统时,通过PlayerInput组件获取InputAction的配置,支持复杂的输入映射和组合键设置。
虚拟鼠标检测
自动检测是否有手柄设备连接,如果检测到手柄则自动创建虚拟鼠标系统,为手柄用户提供鼠标指针功能。
轴值获取处理
统一处理各种轴的输入值,包括鼠标移动、手柄摇杆、键盘方向键等,通过switch语句针对不同轴类型进行特殊处理。
按键检测机制
支持按键按下、持续按下、释放等多种状态检测,同时处理组合键和连续按键的识别。
ABC系统深度集成
与技能系统、移动系统、UI系统等深度集成,提供标准化的输入接口,确保所有ABC组件都能正确响应输入事件。
UI系统
ABC UI系统是应用层的核心界面管理模块,负责处理所有游戏界面的显示、交互和状态更新。它通过ABC_IconUI、ABC_IconController和ABC_StateManager的UI组件,实现了技能图标、状态显示、生命值界面、拖拽交互等丰富的UI功能,为玩家提供了直观的游戏操作界面和状态反馈。
功能实现
/// <summary>
/// If true then the icon will be disabled when the ability is disabled
/// </summary>
[HideInInspector]
[Tooltip("If true then the icon will be disabled when the ability is disabled")]
public bool disableWithAbility = true;/// <summary>
/// If true then an Substitute ability will take this icon for as long as the main ability is disabled
/// </summary>
public bool substituteAbilityWhenDisabled = false;/// The ID of the Substitute ability this IconUI relates too when the main ability is disabled. ID is used so ability names can be changed without breaking the setup.
/// </summary>
[Tooltip("ID of the Substitute ability which will activate when the main ability is disabled")]
[HideInInspector]
public List<int> iconUISubstituteAbilityIDs = new List<int>();/// <summary>
/// used for inspector, keeps track of what ability is currently chosen for the Substitute IconUI
/// </summary>
[HideInInspector]
public int IconUISubstituteAbilityListChoice = 0;/// <summary>
/// If true then the UI will display countdown information and grapics
/// </summary>
[Tooltip("If true then the UI will display countdown information and grapics")]
[HideInInspector]
public bool displayCountdown = true;/// <summary>
/// Image object which will be placed over the image and will fill down
/// </summary>
[Tooltip("Image object which will be placed over the image and will fill down ")]
[HideInInspector]
public ABC_ImageReference countdownFillOverlay = new ABC_ImageReference();
这个配置系统定义了图标UI的核心行为。disableWithAbility确保图标与技能状态同步,当技能被禁用时图标也会自动禁用。substituteAbilityWhenDisabled允许设置替代技能,当主技能被禁用时替代技能会接管图标。displayCountdown控制是否显示冷却时间,countdownFillOverlay提供冷却进度条的视觉反馈。
/// <summary>
/// Method which listens to the button onclick event. Once called it will then call it's own delegate event for other components to know when the icon was clicked
/// </summary>
private void OnClickEvent() {IconUI.Click();
}/// <summary>
/// Creates a gameobject which is a clone of the icon this component is attached too.
/// </summary>
/// <returns>A new gameobject which is a clone of the icon this component is attached too</returns>
private GameObject CreateDragIcon() {//Create new gameobjectGameObject retval = new GameObject("Icon");//Add the raw image and update it's texture to match RawImage image = retval.AddComponent<RawImage>();image.texture = GetComponent<RawImage>().texture;image.raycastTarget = false;//Mimic the size RectTransform iconRect = retval.GetComponent<RectTransform>();iconRect.sizeDelta = GetComponent<RectTransform>().sizeDelta;//Make sure Icon is always on top of GUICanvas canvas = GetComponentInParent<Canvas>();if (canvas != null) {retval.transform.SetParent(canvas.transform, true);retval.transform.SetAsLastSibling();}return retval;
}/// <summary>
/// Will swap this icon with the icon which is currently selected by the system
/// </summary>
private void SwapWithSelectedIcon() {//If we are not set to swap or there is no current selected icon then end hereif (selectedIcon == null || this.IconUI.actionType != ActionType.Dynamic)return;//Store the type settings from this icon into a placeholderABC_IconUI iconUIPlaceHolder = new ABC_IconUI();iconUIPlaceHolder.CloneTypeSettings(this.IconUI);//Copy icon type settings from the icon we are swapping with this.IconUI.CloneTypeSettings(selectedIcon.IconUI);//Now copy over to the icon we are swapping with the icon type settings which this icon use to have (stored in the placeholder just created)selectedIcon.IconUI.CloneTypeSettings(iconUIPlaceHolder);
}
拖拽交互系统实现了完整的图标拖拽功能。OnClickEvent处理点击事件,CreateDragIcon创建跟随鼠标的拖拽图标,确保图标始终显示在最上层。SwapWithSelectedIcon实现了图标交换功能,通过CloneTypeSettings方法复制图标设置,支持动态图标的位置交换。
/// <summary>
/// Text object which will show stat value
/// </summary>
public ABC_TextReference textStatValue;/// <summary>
/// Variable determining if text is showing
/// </summary>
public bool textShowing = false;/// <summary>
/// If true then the text will only show when the entity is selected.
/// </summary>
public bool onlyShowTextWhenSelected = false;/// <summary>
/// Returns the stat value taking into consideration any boosts
/// </summary>
/// <returns>Value of the stat</returns>
public float GetValue() {return this.statValue + this.statIncreaseValue;
}/// <summary>
/// Sets the stat to the value provided
/// </summary>
/// <param name="Value">Value to set the stat too</param>
public float SetValue(float Value) {return this.statIncreaseValue = Value;
}/// <summary>
/// Will modify the value by the amount provided
/// </summary>
/// <param name="Amount">Amount to increase or decrease the stat value by</param>
public void AdjustValue(float Amount) {this.statIncreaseValue += Amount;
}
状态管理器的UI系统负责显示实体的各种状态信息。textStatValue引用显示属性值的文本组件,textShowing控制文本显示状态,onlyShowTextWhenSelected实现选择性显示功能。GetValue方法返回考虑增益后的最终属性值,SetValue和AdjustValue提供属性值的设置和调整功能。
工作流程
UI初始化处理
系统在启动时创建Canvas、EventSystem等基础UI组件,设置渲染模式和输入模块,为后续的UI元素提供显示和交互基础。
图标创建管理
根据技能配置自动创建对应的UI图标,设置图标纹理、大小、位置等属性,建立图标与技能之间的关联关系。
事件监听机制
监听鼠标点击、悬停、拖拽等用户交互事件,通过Unity的EventSystem系统实现跨平台的输入处理。
状态更新处理
实时监控技能状态、冷却时间、生命值等游戏数据,自动更新对应的UI显示内容,确保界面与游戏状态同步。
交互处理机制
处理图标之间的交换、复制、删除等复杂交互操作,支持动态图标的重排和静态图标的保护。
拖拽管理功能
实现完整的拖拽系统,包括拖拽图标的创建、跟随鼠标移动、放置检测等功能,提供直观的图标管理体验。
显示更新机制
根据游戏状态和用户设置动态更新UI显示,包括显示/隐藏、透明度调整、动画效果等。
ABC系统深度集成
与技能系统、状态系统、输入系统等深度集成,确保UI能够正确响应游戏事件和用户操作。
辅助层 (Support Layer)
武器系统
ABC武器系统是辅助层的重要组成部分,负责角色武器的管理、切换、对象池、特效、装备与卸载等功能。它不仅为技能系统提供了丰富的表现力,还通过对象池机制优化了性能,支持多种武器类型和扩展能力。
功能实现
// 位置:Assets/ABC/Scripts/ABC-Classes/ABC_Utilities.cs
public static GameObject abcPool {get {if (_abcPool == null) {if (GameObject.Find("ABC*_Pool") == null) {_abcPool = new GameObject("ABC*_Pool made from Code");Debug.Log("A pool of ability objects (ABC*_Pool) has been created automatically");_abcPool.name = "ABC*_Pool";} else {_abcPool = GameObject.Find("ABC*_Pool");}if (_abcPool.GetComponent<ABC_MbSurrogate>() == null)_abcPool.AddComponent<ABC_MbSurrogate>();}return _abcPool;}
}
ABC系统通过全局对象池ABC*_Pool集中管理所有武器、技能相关的GameObject。这样可以避免频繁的实例化和销毁,提升运行效率。每次需要回收武器对象时,都会放回这个池子,便于后续复用。
// 位置:Assets/ABC/Scripts/ABC-Classes/ABC_Utilities.cs
public static void PoolObject(GameObject Obj, float Delay = 0f) {if (Obj == null)return;if (Delay > 0f) {mbSurrogate.StartCoroutine(PoolObjectAfterDuration(Obj, Delay));return;}Obj.transform.SetParent(abcPool.transform);Obj.SetActive(false);
}
当武器对象不再需要时(如切换、丢弃、技能结束),会通过PoolObject方法回收到对象池中。支持延迟回收,确保特效等能完整播放。这样大大减少了GC压力和运行时卡顿。
// 位置:Assets/ABC/Scripts/ABC-Components/ABC_Controller.cs
public void CreateObjectPools() {foreach (Weapon weapon in CurrentWeapons) {weapon.CreateObjectPools();}// 省略:能力组、技能等的对象池创建
}
每个角色的ABC_Controller会在初始化时为所有已装备武器批量创建对象池,提前生成好需要的武器对象和特效,确保切换和使用时能瞬时响应。
// 位置:Assets/ABC/Scripts/ABC-Components/ABC_Controller.cs
public void EquipWeapon(int weaponID) {// 省略查找逻辑// 1. 生成武器对象// 2. 挂载到角色手部// 3. 激活相关特效// 4. 更新当前武器状态
}
public void UnequipWeapon() {// 1. 回收武器对象到对象池// 2. 关闭特效// 3. 清空当前武器状态
}
装备武器时,系统会从对象池中取出武器对象,挂载到角色指定部位,并激活相关特效。卸载时则回收到池中,关闭所有相关表现,保证资源高效利用。
// 位置:Assets/ABC/Scripts/ABC-Components/ABC_Controller.cs
public void ActivateAbility(int abilityID) {// 1. 检查当前武器是否支持该技能// 2. 播放武器特效// 3. 触发技能逻辑
}
武器系统与技能系统深度集成,技能的释放会自动检测当前武器类型,决定是否能释放、播放何种特效、应用何种表现,极大丰富了战斗体验。
工作流程
- 角色初始化时,批量创建所有武器对象池
- 玩家切换武器时,从对象池中取出对应武器对象,挂载到角色手部。
- 触发技能时,检测当前武器类型,播放武器特效并执行技能逻辑。
- 武器卸载或切换时,将武器对象回收到对象池,关闭相关特效。
数据管理系统
ABC数据管理系统是辅助层的核心模块,负责游戏内所有ABC相关数据的保存、加载、加密、持久化等功能。它支持本地存档和数据库扩展,保障所有系统的数据安全和一致性,支持场景切换、对象状态恢复、增量更新等高级功能。
功能实现
/// <summary>
/// Will save all ABC related data in the scene, creating an SaveMaster file locally
/// </summary>
public void SaveGameLocally() {//Grab save path if definedstring path = this.saveFilePath + "/Saves";//If using persistant data path then use this insteadif (this.usePersistantDataPath)path = Application.persistentDataPath + "/Saves";//Create Saves folder if it doesn't existDirectory.CreateDirectory(path);//Create the main save file (The Save Master)string saveFile = CreateSaveMaster(this.key32Char);//Save the file to the locationFile.WriteAllText(path + "/" + this.saveFileName + "-ABCSave.json", saveFile);Debug.Log("Game Saved Locally to: " + path);
}
这是ABC数据管理系统的核心保存方法。首先它会根据配置决定使用自定义路径还是Unity的持久化数据路径。Directory.CreateDirectory确保保存目录存在,如果不存在会自动创建。然后调用CreateSaveMaster方法生成包含所有游戏数据的JSON字符串,最后使用File.WriteAllText将加密后的数据写入本地文件。文件命名格式为"存档名-ABCSave.json",这样可以支持多个存档文件并存。
/// <summary>
/// Will load all data from the save defined in the SaveManager settings
/// </summary>
public void LoadGameLocally(bool LoadPersistantObjectsOnly = false) {//Grab save path if defined where the save should existstring path = this.saveFilePath + "/Saves";//If using persistant data path then use this insteadif (this.usePersistantDataPath)path = Application.persistentDataPath + "/Saves";//Check if file exists if it doesn't then log to consoleif (File.Exists(path + "/" + this.saveFileName + "-ABCSave.json") == false) {Debug.LogError("Loading Failed, File " + this.saveFileName + " does not exist in the following location: " + path);return;}//Load the game using the save file at the path and name configured in settingsStartCoroutine(LoadSaveMaster(File.ReadAllText(path + "/" + this.saveFileName + "-ABCSave.json"), LoadPersistantObjectsOnly, this.key32Char));Debug.Log("Game Loading Completed");
}
这是对应的加载方法,与保存方法形成完整的存取循环。它首先构建与保存时相同的路径,然后使用File.Exists检查存档文件是否存在,避免文件不存在时的错误。如果文件存在,使用File.ReadAllText读取整个文件内容,然后通过协程LoadSaveMaster异步处理数据的解密、反序列化和对象状态恢复。LoadPersistantObjectsOnly参数允许只加载持久化对象,这在场景切换时特别有用。
/// <summary>
/// Will create ABC save game data for the objects provided
/// </summary>
public static string CreateSaveData(List<GameObject> ObjectsToSave, string CryptoKey = "^mABj*sj+_{56MMRBJB&&SGRHAJBKM'z") {//Create return objectSaveData retval = new SaveData();//Look through each object foreach (GameObject objABC in ObjectsToSave) {//Create the game data which will be storedGameData gameData = new GameData(objABC.name, objABC.GetInstanceID(), objABC.transform.position, objABC.transform.rotation, objABC.gameObject.activeInHierarchy);//Retrieve the ABC Controller components for the objectABC_Controller abcController = objABC.GetComponent<ABC_Controller>();//If ABC Controller is found then store it's dataif (abcController != null)gameData.dataABC_Controller = JsonUtility.ToJson(abcController);//Retrieve the ABC StateManager components for the objectABC_StateManager abcStateManager = objABC.GetComponent<ABC_StateManager>();//If ABC StateManager is found then store it's dataif (abcStateManager != null)gameData.dataABC_StateManager = JsonUtility.ToJson(abcStateManager);//Retrieve the ABC WeaponPickUp components for the objectABC_WeaponPickUp abcWeaponPickUp = objABC.GetComponent<ABC_WeaponPickUp>();//If ABC WeaponPickUp is found then store it's dataif (abcWeaponPickUp != null)gameData.dataABC_WeaponPickUp = JsonUtility.ToJson(abcWeaponPickUp);//add save data to save masterretval.saveData.Add(gameData);}// if not in debug mode then encrypt, else don't and return the save dataif (CryptoKey != "")return Convert.ToBase64String(Encrypt(JsonUtility.ToJson(retval), CryptoKey));elsereturn JsonUtility.ToJson(retval);
}
这是数据序列化的核心方法,它遍历所有需要保存的GameObject,为每个对象创建一个GameData实例来存储基本信息(名称、ID、位置、旋转、激活状态)。然后检查对象上是否有ABC相关的组件(Controller、StateManager、WeaponPickUp),如果有就使用JsonUtility.ToJson将组件数据序列化为JSON字符串。最后,如果提供了加密密钥,就调用Encrypt方法加密整个数据包,并转换为Base64字符串;否则直接返回未加密的JSON。这种设计既保证了数据的完整性,又提供了可选的安全性。
/// <summary>
/// Will load the save data provided
/// </summary>
public static void LoadSaveData(string Data, bool LoadTransform = false, bool LoadEnableState = false, string CryptoKey = "^mABj*sj+_{56MMRBJB&&SGRHAJBKM'z", List<String> OnlyLoadObjectName = null) {//Create save data object ready to load the data intoSaveData savedData = new SaveData();//If not in debug mode then decrypt the encrypted fileif (CryptoKey != "")JsonUtility.FromJsonOverwrite(Decrypt(Convert.FromBase64String(Data), CryptoKey), savedData);elseJsonUtility.FromJsonOverwrite(Data, savedData);//Get all objects in scene to load intoList<GameObject> allABCObjs = ABC_Utilities.GetAllObjectsInScene();//Loop through the master Saved data retrieving all the objects we are going to load data into foreach (GameData data in savedData.saveData) {//If we are only loading certain objects/names and the current data being loaded does not relate to any of those entities then skip if (OnlyLoadObjectName != null && OnlyLoadObjectName.Any(y => y == data.objName) == false)continue;//declare the object which will be loading data in this cycleGameObject objToLoad;//If there is more then 1 object with the same name in our current play which has the same name then try and search for an object with the same name and instance ID recordedif (allABCObjs.Count(o => o.name == data.objName) > 1) {objToLoad = allABCObjs.Where(o => o.name == data.objName && o.gameObject.GetInstanceID() == data.objID).FirstOrDefault();//If we still can't find the entity then we will pick one duplicate named object at random and remove it so next time the other one is restoredif (objToLoad == null) {objToLoad = allABCObjs.Where(o => o.name == data.objName).FirstOrDefault();allABCObjs.Remove(objToLoad);}} else { // else no duplicate of this name exists so retrieve an object with the same nameobjToLoad = allABCObjs.Where(o => o.name == data.objName).FirstOrDefault();}}
}
这是数据加载的第一阶段,负责解密和对象匹配。首先根据是否有加密密钥来决定解密流程:如果有密钥,先用Convert.FromBase64String解码,再用Decrypt解密,最后用JsonUtility.FromJsonOverwrite反序列化;否则直接反序列化。然后获取场景中所有对象,开始匹配过程。对象匹配采用了智能策略:如果场景中有多个同名对象,优先通过名称+实例ID精确匹配;如果精确匹配失败,选择第一个同名对象并从列表中移除,避免下次冲突。这种设计很好地处理了复杂场景中的对象识别问题。
//If we found an object then load all it's data
if (objToLoad != null) {//If enabled in parameter then enable/disable object from the saveif (LoadEnableState == true) {objToLoad.SetActive(data.objEnabled);}//Retrieve the ABC Controller components for the objectABC_Controller abcController = objToLoad.GetComponent<ABC_Controller>();//If ABC Controller is found then load it's dataif (abcController != null) {//Clear current graphics before overwriting the tracking listsabcController.ClearAllPools();JsonUtility.FromJsonOverwrite(data.dataABC_Controller, abcController);//Reload Controllerif (objToLoad.activeInHierarchy == true)abcController.Reload();}//Retrieve the ABC StateManager components for the objectABC_StateManager abcStateManager = objToLoad.GetComponent<ABC_StateManager>();//If ABC StateManager is found then load it's dataif (abcStateManager != null) {JsonUtility.FromJsonOverwrite(data.dataABC_StateManager, abcStateManager);//Reload StateManagerif (objToLoad.activeInHierarchy == true)abcStateManager.Reload();}//If enabled in parameter then load the transform and rotation from the saveif (LoadTransform == true) {objToLoad.transform.position = data.transformPosition;objToLoad.transform.rotation = data.transformRotation;}
}
这是数据加载的第二阶段,负责具体的组件状态恢复。首先根据LoadEnableState参数决定是否恢复对象的激活状态。然后按序恢复各个ABC组件:对于ABC_Controller,先调用ClearAllPools()清理当前的对象池和图形资源,避免数据冲突,再用JsonUtility.FromJsonOverwrite覆盖组件数据,最后调用Reload()重新初始化组件;ABC_StateManager的处理类似,但不需要清理池。最后根据LoadTransform参数决定是否恢复Transform数据。这种分阶段、分组件的恢复方式确保了数据的完整性和组件间的正确关联。
public static byte[] Encrypt(string original, string key) // key must be 32chars
{byte[] encrypted = null;try {byte[] iv = Encoding.ASCII.GetBytes("1234567890123456");byte[] keyBytes = Encoding.ASCII.GetBytes(key);using (RijndaelManaged myRijndael = new RijndaelManaged()) {myRijndael.Key = keyBytes;myRijndael.IV = iv;encrypted = EncryptStringToBytes(original, myRijndael.Key, myRijndael.IV);}} catch (Exception e) {Debug.LogFormat("Error: {0}", e.Message);}return encrypted;
}private static byte[] EncryptStringToBytes(string plainText, byte[] Key, byte[] IV) {if (plainText == null || plainText.Length <= 0)throw new ArgumentNullException("plainText");if (Key == null || Key.Length <= 0)throw new ArgumentNullException("Key");if (IV == null || IV.Length <= 0)throw new ArgumentNullException("IV");byte[] encrypted;using (RijndaelManaged rijAlg = new RijndaelManaged()) {rijAlg.Key = Key;rijAlg.IV = IV;ICryptoTransform encryptor = rijAlg.CreateEncryptor(rijAlg.Key, rijAlg.IV);using (MemoryStream msEncrypt = new MemoryStream()) {using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) {using (StreamWriter swEncrypt = new StreamWriter(csEncrypt)) {swEncrypt.Write(plainText);}encrypted = msEncrypt.ToArray();}}}return encrypted;
}
这是ABC数据管理系统的安全核心,使用AES(Rijndael)算法加密存档数据。Encrypt方法是公共接口,要求32字符的密钥,使用固定的16字节初始化向量(IV)。EncryptStringToBytes是实际的加密实现,它创建RijndaelManaged对象设置密钥和IV,然后创建加密器ICryptoTransform。加密过程使用了三层流的设计模式:MemoryStream作为底层存储,CryptoStream处理加密转换,StreamWriter负责字符串写入。这种流式设计既保证了内存效率,又确保了加密的完整性。所有资源都使用using语句自动管理,避免内存泄漏。
public static string Decrypt(byte[] soup, string key) {string outString = "";try {byte[] iv = Encoding.ASCII.GetBytes("1234567890123456");byte[] keyBytes = Encoding.ASCII.GetBytes(key);using (RijndaelManaged myRijndael = new RijndaelManaged()) {myRijndael.Key = keyBytes;myRijndael.IV = iv;outString = DecryptStringFromBytes(soup, myRijndael.Key, myRijndael.IV);}} catch (Exception e) {Debug.LogFormat("Error: {0}", e.Message);}return outString;
}private static string DecryptStringFromBytes(byte[] cipherText, byte[] Key, byte[] IV) {if (cipherText == null || cipherText.Length <= 0)throw new ArgumentNullException("cipherText");if (Key == null || Key.Length <= 0)throw new ArgumentNullException("Key");if (IV == null || IV.Length <= 0)throw new ArgumentNullException("IV");string plaintext = null;using (RijndaelManaged rijAlg = new RijndaelManaged()) {rijAlg.Key = Key;rijAlg.IV = IV;ICryptoTransform decryptor = rijAlg.CreateDecryptor(rijAlg.Key, rijAlg.IV);using (MemoryStream msDecrypt = new MemoryStream(cipherText)) {using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) {using (StreamReader srDecrypt = new StreamReader(csDecrypt)) {plaintext = srDecrypt.ReadToEnd();}}}}return plaintext;
}
这是与加密方法完全对应的解密实现,确保了数据的可逆性。Decrypt方法接收加密后的字节数组和相同的密钥,使用与加密时相同的IV确保解密的正确性。DecryptStringFromBytes是核心解密逻辑,同样使用三层流的设计:MemoryStream读取加密的字节数据,CryptoStream执行解密转换,StreamReader将解密后的字节转换回字符串。解密器ICryptoTransform与加密器配对,使用CryptoStreamMode.Read模式。这种对称设计保证了加密和解密过程的一致性,同时通过异常处理确保了操作的健壮性。
工作流程
系统首先触发保存或加载操作,然后根据操作类型分流处理——保存时收集所有ABC对象数据,序列化为JSON格式,进行AES加密后写入本地文件或数据库;加载时则反向执行,从本地文件或数据库读取数据,进行AES解密,JSON反序列化,查找场景中对应对象并恢复其状态和数据,最后重新加载组件完成整个流程。
全局管理系统
ABC全局管理系统是辅助层的核心配置和初始化模块,负责统一管理所有ABC系统的全局配置、角色创建、组件预设、游戏类型适配等功能。它通过ABC_GlobalPortal、ABC_GlobalElement等核心组件,为整个ABC框架提供统一的配置入口和系统初始化服务。
功能实现
[System.Serializable]
public class ABC_GlobalPortal : ScriptableObject {ABC_GlobalPortal() {}public GameObject ComponentPreset;public List<GameObject> UI = new List<GameObject>();public List<GameObject> tagHolders = new List<GameObject>();public CharacterType characterType = CharacterType.Player;public CharacterIntegrationType characterIntegrationType = CharacterIntegrationType.ABC;public GameType gameType = GameType.Action;public bool addComponentPresets = true;public bool persistentCrosshairAestheticMode = false;public bool setupGameTypeTargetting = true;public bool convertAbilitiesToGameType = false;public bool alwaysShowUI = true;public PointClickType clickType = PointClickType.Click;public bool setupForWeaponsAndAbilities = true;public bool addUI = true;public bool enableAI = true;public AIType typeAI = AIType.CloseCombat;public bool addCamera = true;public bool invertYAxis = false;public List<GameObject> gameCameras = new List<GameObject>();public bool addMovement = true;public bool addMovementPresets = true;public bool enableParkour = false; public RuntimeAnimatorController AniController;public bool enableLockOnMovement = false;public bool enableJumping = false;public bool enableCameraRotation = true;public GameObject weaponHolderAdjustmentObject;public GameObject gunHolderAdjustmentObject;public bool displayABCTemplates = true;public bool displayABCElements = true;public bool displayWelcomeOnStartUp = true;public enum CharacterType {Player,Enemy,Friendly}public enum AIType {CloseCombat,Ranged}public enum CharacterIntegrationType {ABC,GameCreator2}public enum GameType {Action,FPS,TPS,RPGMMO,MOBA,TopDownAction}public enum PointClickType {Hover,Click}
}
这是ABC全局管理系统的核心配置类,继承自ScriptableObject以便在Unity编辑器中直接配置。它定义了角色类型(玩家、敌人、友军)、游戏类型(动作、FPS、TPS等)、集成类型(ABC原生或GameCreator2)等枚举,以及各种功能开关(添加组件预设、启用AI、添加移动系统等)。ComponentPreset存储默认的组件配置模板,UI和tagHolders列表管理全局UI元素和标签持有者。这种设计允许开发者通过一个统一的配置界面来控制整个ABC系统的行为。
public async void CreateCharacter() {//Add ABC Controller if not already addedabcManager = CharacterObject.GetComponent<ABC_Controller>();if (abcManager == null)abcManager = CharacterObject.AddComponent<ABC_Controller>();//Add ABC StateManager if not already addedstateManager = CharacterObject.GetComponent<ABC_StateManager>();if (stateManager == null)stateManager = CharacterObject.AddComponent<ABC_StateManager>();await Task.Delay(25);this.ConvertAbilitiesToGameType();this.AddComponentPresets();this.AddPlayerInput();this.SetupCharacterTargetting();this.SetupTagsAndWeaponHolders();this.AddCharacterUI();this.AddGameCamera();this.AddMovement();
}
这是角色创建的核心方法,采用异步设计确保组件添加的稳定性。首先检查并添加ABC_Controller和ABC_StateManager两个核心组件,然后通过Task.Delay(25)确保组件完全初始化。接着按顺序执行各个配置步骤:转换技能到游戏类型、添加组件预设、设置玩家输入、配置角色瞄准、设置标签和武器持有者、添加角色UI、添加游戏相机、添加移动系统。这种分步骤的设计确保了每个系统都能正确初始化和配置。
public async void AddComponentPresets() {if (GetTarget.FindProperty("addComponentPresets").boolValue == false) {return;}#region Import Base Data//Record current abilities on entityList<ABC_Ability> abilityImport = new List<ABC_Ability>();foreach (ABC_Ability a in abcManager.Abilities) {ABC_Ability newAbility = new ABC_Ability();JsonUtility.FromJsonOverwrite(JsonUtility.ToJson(a), newAbility);abilityImport.Add(newAbility);}//Record current weapons on entityList<ABC_Controller.Weapon> weaponImport = new List<ABC_Controller.Weapon>();foreach (ABC_Controller.Weapon w in abcManager.Weapons) {ABC_Controller.Weapon newWeapon = new ABC_Controller.Weapon();JsonUtility.FromJsonOverwrite(JsonUtility.ToJson(w), newWeapon);weaponImport.Add(newWeapon);}//Overwrite ABC manager with the defaults from global portal JsonUtility.FromJsonOverwrite(JsonUtility.ToJson(globalPortal.ComponentPreset.GetComponent<ABC_Controller>()), abcManager);//reImport any abilities that already existedif (abilityImport.Count > 0)abcManager.Abilities.AddRange(abilityImport);//reImport any weapons that already existedif (weaponImport.Count > 0)abcManager.Weapons.AddRange(weaponImport);
}
这个方法负责应用组件预设配置。首先检查是否启用了组件预设功能,然后备份当前实体的技能和武器数据,使用JsonUtility进行深拷贝确保数据完整性。接着用全局门户的ComponentPreset覆盖当前ABC_Controller的配置,最后重新导入之前备份的技能和武器数据。这种设计既保证了预设配置的应用,又保留了用户已有的自定义数据,实现了配置的合并而非完全替换。
public class ABC_GlobalElement : ScriptableObject {public Texture2D elementIcon;public bool showWeaponPreview = true;public string elementDescription;public List<string> elementTags = new List<string>();public string creationDate = System.DateTime.Now.ToString();public string createdBy = Environment.UserName.ToString();public bool officialABC = false;public GlobalElementType elementType = GlobalElementType.Abilities;public ABC_Controller.Weapon ElementWeapon = null;public List<ABC_Ability> ElementAbilities = new List<ABC_Ability>();public List<ABC_Controller.AIRule> ElementAIRules = new List<ABC_Controller.AIRule>();public enum GlobalElementType {Weapon,Abilities,Effect,AIRules}
}
ABC_GlobalElement是全局元素系统的核心类,用于创建可重用的技能、武器、效果和AI规则包。它包含元素的图标、描述、标签、创建信息等元数据,以及具体的游戏元素数据(武器、技能、AI规则)。这种设计允许开发者创建标准化的元素包,可以在不同角色间共享和重用,大大提高了开发效率和一致性。
public IEnumerator AddGlobalElementAtRunTime(ABC_GlobalElement GlobalElement, bool EquipWeapon = false, bool EnableGameTypeModification = false, ABC_GlobalPortal.GameType GameTypeModification = ABC_GlobalPortal.GameType.Action) {///////////////// Global Weaponif (GlobalElement.ElementWeapon != null) {//If weapon already exists then just enable itif (this.Weapons.Where(w => w.globalWeapon == GlobalElement).Count() > 0) {this.EnableWeapon(GlobalElement.ElementWeapon.weaponID, EquipWeapon, false);} else {Weapon newGlobalWeapon = new Weapon();newGlobalWeapon.globalWeapon = GlobalElement;newGlobalWeapon.weaponID = -1;this.Weapons.Add(newGlobalWeapon);}}///////////////// Global Ability if (GlobalElement.ElementAbilities != null) {ABC_Ability newGlobalAbility = new ABC_Ability();newGlobalAbility.globalAbilities = GlobalElement;newGlobalAbility.abilityID = -1;newGlobalAbility.globalAbilitiesEnableGameTypeModification = EnableGameTypeModification;newGlobalAbility.globalAbilitiesGameTypeModification = GameTypeModification;this.Abilities.Add(newGlobalAbility);}///////////////// Global AI foreach (ABC_Controller.AIRule rule in GlobalElement.ElementAIRules) {if (this.AIRules.Where(ai => ai.selectedAIAction == rule.selectedAIAction && ai.AIAbilityID == rule.AIAbilityID).Count() > 0)continue;ABC_Controller.AIRule newRule = new ABC_Controller.AIRule();JsonUtility.FromJsonOverwrite(JsonUtility.ToJson(rule), newRule);AIRules.Add(newRule);}if (Application.isPlaying) {yield return this.InitialiseComponent(true, EquipWeapon ? GlobalElement.ElementWeapon.weaponID : -1);}yield break;
}
这个方法实现了运行时动态添加全局元素的功能。对于武器,先检查是否已存在相同的全局武器,如果存在则直接启用,否则创建新的武器实例。对于技能,创建新的技能实例并设置游戏类型修改参数。对于AI规则,使用深拷贝避免引用问题,并检查重复规则。最后如果游戏正在运行,重新初始化组件以应用新添加的元素。这种设计支持游戏运行时的动态内容更新,为游戏提供了极大的灵活性。
public static ABC_GlobalElement GetGlobalElement(string GlobalElementName, bool IncludeWeapons = true, bool IncludeAbilities = true, bool IncludeEffects = true) {ABC_GlobalElement retVal = null;ABC_GlobalElement[] globalElements = Resources.LoadAll<ABC_GlobalElement>("");for (int i = 0; i < globalElements.Length; i++) {if (globalElements[i].elementType == ABC_GlobalElement.GlobalElementType.Weapon && IncludeWeapons == false)continue;if (globalElements[i].elementType == ABC_GlobalElement.GlobalElementType.Abilities && IncludeAbilities == false)continue;if (globalElements[i].elementType == ABC_GlobalElement.GlobalElementType.Effect && IncludeEffects == false)continue;if (globalElements[i].name == GlobalElementName) {retVal = globalElements[i];}}return retVal;
}
这个静态方法用于从Resources文件夹中查找和获取全局元素。它使用Resources.LoadAll加载所有ABC_GlobalElement类型的资源,然后根据名称匹配目标元素。通过IncludeWeapons、IncludeAbilities、IncludeEffects参数可以过滤特定类型的元素,提高查找效率。这种设计支持按需加载和类型过滤,为全局元素系统提供了灵活的查找机制。
public void ConvertAbilitiesToGameType() {if (globalPortal.convertAbilitiesToGameType == false) {return;}foreach (ABC_Ability ability in abcManager.Abilities) {ability.ConvertToGameType(globalPortal.gameType);}foreach (ABC_Controller.Weapon weapon in abcManager.Weapons) {if (weapon.weaponAbilities != null) {foreach (ABC_Ability weaponAbility in weapon.weaponAbilities) {weaponAbility.ConvertToGameType(globalPortal.gameType);}}}
}
这个方法负责将技能和武器适配到特定的游戏类型。首先检查是否启用了游戏类型转换功能,然后遍历所有技能和武器技能,调用它们的ConvertToGameType方法进行适配。这种设计允许同一套技能和武器在不同游戏类型(如FPS、TPS、RPG等)中自动调整其行为参数,确保游戏体验的一致性和适配性。
工作流程
系统启动时首先加载GlobalPortal全局配置文件,读取角色类型、游戏类型等设置,然后根据配置创建角色并自动添加ABC_Controller和ABC_StateManager等核心组件,接着根据游戏类型(FPS/TPS/RPG等)调整技能和武器行为,配置UI界面和输入系统,最后支持运行时动态添加全局元素实现灵活的内容更新。
一些常见问题
Q:具体来说是怎么采用ABC框架构建了完整的技能、武器、状态管理系统?
A:
技能系统实现总结:
ABC框架采用组件化设计思想,将传统的面向对象技能系统重构为三个核心组件架构。首先,技能基本属性组件负责存储技能的所有静态信息,包括技能的唯一ID、名称、描述、图标、等级、升级条件等基础数据,这些信息统一存储在ScriptableObject文件中,便于数据驱动和扩展。其次,技能释放组件专门负责执行技能释放后的具体效果,包括播放音效、特效,生成实体(如火球、闪电等),应用伤害、治疗,更新冷却时间,以及处理各种状态效果(buff/debuff),确保技能逻辑的模块化和可复用性。最后,技能触发组件定义技能如何被触发,包括基本的按键检测(键盘、鼠标、手柄输入),条件检测(血量、魔法值、距离等前提条件),以及连击检测等复杂触发逻辑。通过这种三组件分离的设计,每个技能都能灵活组合不同的属性、释放效果和触发条件,大大提高了技能系统的可扩展性和维护性。在此基础上,框架还实现了完整的连招系统,当每个技能释放时会输出当前时间戳,系统维护一个技能历史队列记录所有释放过的技能。当玩家尝试释放需要前提条件的技能时,系统会检测按键输入的同时,从队列中读取上一个释放技能的时间戳,计算时间差值并与预设的时间窗口进行比较。如果时间差满足连招需求,系统认为连招成功并继续执行;如果时间差超出窗口范围,则认为连招失败并重置连招序列。这种基于时间戳和队列的连招检测机制,确保了连招系统的准确性和流畅性,为玩家提供了丰富的技能组合体验。
武器系统实现总结:
在ABC框架中,武器系统的实现是高度模块化和数据驱动的。每个武器都对应一个独立的预制体,包含其3D模型、碰撞体和必要的脚本组件,而武器的属性(如名称、类型、描述、图标、伤害、专属技能等)则统一存储在ScriptableObject(SO)文件中,便于管理和扩展。在场景中,武器实体会挂载检测脚本,利用OnTriggerEnter等Trigger相关函数检测与玩家的碰撞,并通过标签判断是否为玩家。如果检测到玩家靠近,就会激活一个预制好的UI画板,弹出“可拾取”提示。当玩家拾取武器时,系统会先检查背包是否有空余空间,如果有,则销毁场景中的武器实体,并在背包中分配一个空插槽,更新其UI显示。背包系统底层是一个动态数组(List),每个插槽是自定义类,包含图标、文本、是否为空等属性。拾取武器后,通常会触发一个事件,驱动背包UI和数据的同步更新。每个背包插槽一般设计为按钮,点击后会弹出一系列可选项(如装备、丢弃等),这些选项通过Unity UI的Button组件和OnClick事件实现逻辑绑定。
装备系统分为仓库(背包)和装备栏两部分。装备栏有固定的槽位(如主手、副手、头盔等),每个槽位只能装备特定类型的装备。当玩家点击“装备”时,系统会从SO文件读取该武器的类型和部位信息,检查对应装备槽是否为空。如果槽位已有装备,则先将原装备放回背包,再将新装备放入装备栏,并更新UI显示。装备的可视化依赖于骨骼绑定:角色模型的骨骼信息由Animator组件管理,装备时通过Animator的GetBoneTransform方法获取目标骨骼的Transform,将武器预制体实例化后设置为该骨骼的子对象,并调整其局部位置和旋转,实现武器随角色动作自然移动。
攻击的实现通常是:玩家按下攻击键后,播放攻击动画,并在动画关键帧插入动画事件,激活武器的碰撞体。此时如果武器的碰撞体与敌人(标签为Enemy)发生Trigger碰撞,就会调用敌人的受伤函数,计算并应用伤害。至于攻击动画的平滑过渡,常见做法是利用Animator的动画过渡(Blend Tree、Transition、CrossFade等),通过参数控制动画状态机的切换,实现连招或连续攻击时动画的自然衔接。这样,整个武器系统从拾取、背包管理、装备、可视化到攻击判定,都实现了高度解耦和可扩展的设计。
状态管理系统实现总结:
在ABC项目中,状态管理系统本质上就是一个有限状态机。首先,我们会用一个枚举类型列举出玩家所有可能的状态,比如Idle(待机)、Run(奔跑)、Attack(攻击)、Hurt(受伤)、Dead(死亡)等。每个状态都对应三个核心事件:进入状态(OnStateEnter)、状态更新(OnStateUpdate)、退出状态(OnStateExit)。当玩家切换到某个新状态时,会先调用当前状态的OnStateExit方法做清理,然后调用新状态的OnStateEnter方法做初始化。每一帧,系统都会调用当前状态的OnStateUpdate方法,在这里根据具体的游戏逻辑判断是否需要切换到其他状态。
比如在Idle状态的OnStateUpdate中,会检测玩家的速度,如果速度大于某个阈值,就调用ChangeState方法切换到Run状态;在Attack状态的OnStateUpdate中,会检测攻击动画是否播放完毕,完毕后切换回Idle状态。ChangeState方法是整个状态切换的核心,它接收目标状态作为参数,自动处理退出当前状态和进入新状态的流程。这样,每个状态的切换逻辑都被封装在各自的Update事件中,状态切换既灵活又清晰,便于维护和扩展。
Q:如何构建多输入系统兼容架构,支持Unity新旧输入系统,如何实现虚拟鼠标、手柄适配等功能?
A:
为了兼容新旧两套输入系统,我们会自己写一个InputManager类,在这个类里用一个布尔变量来决定是否启用新的输入系统。如果启用了新输入系统,就用Input System的API(比如InputAction、PlayerInput等)来检测输入;如果没有启用,就用旧输入系统的API(比如Input.GetKeyDown、Input.GetAxis等)来检测输入。所有输入检测都通过InputManager来统一处理,项目其他地方只需要调用InputManager的方法,不用关心底层用的是哪套输入系统。其实本质上就是加了一个布尔变量,根据它来判断用哪种方式获取输入,没有什么特别复杂的新东西。
至于虚拟鼠标,在新输入系统下,Unity已经内置了Virtual Mouse功能,可以直接用Input System的VirtualMouse设备,根据手柄输入实时移动虚拟鼠标的位置,实现用手柄控制鼠标光标的效果,并且可以用手柄按钮模拟鼠标点击,UI界面就能被手柄操作。而在旧输入系统下,我们需要自己新建一个Vector2变量来表示虚拟鼠标的位置,然后通过Input.GetAxis等方法获取手柄的输入,根据手柄的输入速度来不断修改这个2D变量的位置,从而模拟鼠标的移动效果,同时监听手柄按钮来模拟鼠标点击,让UI界面也能响应手柄操作。这样就实现了无论用哪种输入系统,都能让手柄等非鼠标设备像鼠标一样操作游戏界面。
Q:ABC_GlobalPortal是什么呢?有何特殊之处?
A:
ABC_GlobalPortal是ABC框架中的一个全局配置中心,它本质上是一个ScriptableObject(SO文件),也就是一个专门用来存储和管理全局数据的“数据容器”。你可以把它理解为ABC系统的“总控制台”或者“游戏大管家”。它不会像脚本那样挂在某个游戏对象上,而是作为一个资源文件,集中保存了整个项目中与ABC相关的各种全局设置。
在ABC_GlobalPortal里,你可以配置很多游戏的核心参数,比如角色类型(玩家、敌人、友军)、游戏类型(动作、射击、MOBA、RPG等)、是否启用AI、是否添加UI、是否启用相机和移动系统、是否允许跑酷、是否反转相机Y轴、是否启用跳跃、是否显示欢迎界面等等。除此之外,它还可以管理全局的UI预制体、相机预制体、标签、动画控制器等资源。通过这些配置,开发者可以一键切换不同的游戏模式和功能模块,让项目适配多种玩法和需求。
ABC_GlobalPortal的最大特点是集中管理和高度可配置。所有ABC相关的全局设置都在这里统一调整,避免了配置分散、数据混乱的问题。比如你想让所有角色都能跳跃、所有场景都显示某个UI,只需要在GlobalPortal里勾选相关选项即可,无需到处改代码。它还支持通过专门的编辑器窗口进行可视化操作,开发者可以在Unity编辑器里像填表一样快速配置各种参数,非常方便。
此外,ABC_GlobalPortal还支持根据不同的游戏类型自动调整系统行为,比如切换到FPS模式会自动启用第一人称相机,切换到MOBA模式会启用俯视角相机等。它还可以作为各个子系统(如武器、技能、AI、UI等)的开关,灵活控制哪些功能需要启用,哪些可以关闭。
Q:如何理解数据持久化?ASE加密算法是什么?
A:
数据持久化,指的是将程序运行过程中产生的重要数据(比如玩家进度、设置、背包、分数等)保存到本地存储或云端,使得即使游戏关闭或设备重启,这些数据也不会丢失。数据持久化的目的是让玩家下次进入游戏时,能够继续上次的进度或保留之前的个性化设置,是现代游戏和应用开发中非常重要的一个环节。
Unity常见的数据持久化的方法:
AES(Advanced Encryption Standard,高级加密标准)是一种非常常用的对称加密算法,“对称加密”意思是:加密和解密都用同一个密钥(密码)。
在C#(包括Unity)里,System.Security.Cryptography命名空间下就有一个叫做Aes的类(有时叫AesManaged或AesCryptoServiceProvider),它就是专门用来做AES加密和解密的。
你只需要引用这个命名空间,然后用Aes.Create()就能获得一个AES对象,之后就可以设置密钥(Key)、初始向量(IV),然后用它的CreateEncryptor()和CreateDecryptor()方法来加密和解密数据。