一、概述
游戏是一个以探索解锁新地区,采集资源以进行地区建设来防范定时对地区发动攻击的动作战斗游戏,核心流程如下:
暂时无法在飞书文档外展示此内容
二、具体功能实现方案
注:代码为临时编写用于示例实现方式,主要为设计思路分享
一、动作战斗系统实现
1. 设计相关
角色的移动跳跃可以通过普通的角色控制器实现,不做过多赘述。对于战斗系统,原则上希望原理相对简单,实际的上手需要一定门槛,方案为轻攻击,重攻击,跳跃和躲避的排列组合以此派生,核心在于使用不同武器时有不同的连段和不同的躲避性能,以此在游戏内通过随机掉落不同的武器既增添了战斗系统的丰富度同时也限制了通过掉落物获得爆发性的提升可能。
2. 实现相关
战斗系统的设计包含几个要点:
- 相对精确的碰撞检测
- 反馈状态系统(包含受击效果、攻击效果等)
- 连击的派生与取消
- 帧的动画状态控制
1. 对于碰撞检测,可以考虑使用使用ECS系统单纯实现,如:
//攻击实体
public struct HitBox : IComponentData
{
public float3 Center;
public float Radius;
public Entity Owner;
public int DamageValue;
}
//受击实体
public struct HurtBox : IComponentData
{
public float3 Center;
public float Radius;
public Entity Owner;
}
// 碰撞检测系统
public class HitDetectionSystem : SystemBase
{
protected override void OnUpdate()
{
Entities.WithAll<HitBox>().ForEach((Entity hitEntity, ref HitBox hitBox) =>
{
Entities.WithAll<HurtBox>().ForEach((Entity hurtEntity, ref HurtBox hurtBox) =>
{
if (hitBox.Owner != hurtBox.Owner && math.distance(hitBox.Center, hurtBox.Center) < hitBox.Radius + hurtBox.Radius)
{
var damageEvent = new DamageEvent
{
Attacker = hitBox.Owner,
Target = hurtBox.Owner,
Damage = hitBox.DamageValue
};
EntityManager.AddComponentData(hurtEntity, damageEvent);
}
}).Run();
}).Run();
}
}
以上方式可以实现,但是复杂场景下可能会有性能问题,可以使用Job System进行性能优化,如:
public struct HitDetectionJob : IJobParallelFor
{
public NativeArray<HitBoxData> hitBoxes;
public NativeArray<HurtBoxData> hurtBoxes;
public NativeQueue<DamageEvent>.ParallelWriter damageEvents;
//碰撞检测
public void Execute(int index)
{
var hitBox = hitBoxes[index];
foreach (var hurtBox in hurtBoxes)
{
if (hitBox.owner != hurtBox.owner &&
math.distance(hitBox.position, hurtBox.position) < hitBox.radius + hurtBox.radius) {
damageEvents.Enqueue(new DamageEvent
{
attacker = hitBox.owner,
target = hurtBox.owner,
damage = hitBox.damage
});
}
}
}
}
2. 受击反馈系统可以以如下方式实现,如:
private Animator animator;
private Rigidbody rigidbody;
private Coroutine currentReaction;
public float hitStunDuration = 0.3f;
public float launchForce = 5f;
public AnimationCurve hitSlowCurve;
public void ProcessHit(HitDirection direction, HitIntensity intensity)
{
if (currentReaction != null)
{
StopCoroutine(currentReaction);
}
currentReaction = StartCoroutine(HitReactionRoutine(direction, intensity));
}
private IEnumerator HitReactionRoutine(HitDirection dir, HitIntensity intensity)
{
//触发受击相关动画
animator.SetTrigger(GetHitTriggerName(dir, intensity));
// 受击相关物理反馈
Vector3 force = GetForceDirection(dir) * intensity.Power;
rigidbody.AddForce(force, ForceMode.VelocityChange);
// 受击后时间变化
Time.timeScale = 0.2f;
yield return new WaitForSecondsRealtime(0.1f);
float timer = 0f;
while (timer < hitStunDuration) {
Time.timeScale = Mathf.Lerp(0.2f, 1f, hitSlowCurve.Evaluate(timer / hitStunDuration));
timer += Time.unscaledDeltaTime;
yield return null;
}
Time.timeScale = 1f;
}
private string GetHitTriggerName(HitDirection dir, HitIntensity intensity)
{
return $"Hit_{dir.ToString()}_{intensity.ToString()}";
}
同时受击可以配合相对动态的物理材质控制提升效果,如:
private Dictionary<Collider, PhysicMaterial> originalMaterials = new Dictionary<Collider, PhysicMaterial>();
public void ApplyTemporaryMaterial(Collider collider, PhysicMaterial material, float duration)
{
StartCoroutine(MaterialRoutine(collider, material, duration));
}
private IEnumerator MaterialRoutine(Collider collider, PhysicMaterial material, float duration)
{
if (!originalMaterials.ContainsKey(collider))
{
originalMaterials[collider] = collider.material;
}
collider.material = material;
yield return new WaitForSeconds(duration);
collider.material = originalMaterials[collider];
}
3. 连击系统的实现,认为是通过状态机控制相对方便,如
private int currentComboStep;
private float comboResetTimer;
private bool canAcceptNextInput;
public class ComboSequence {
public string AnimationState;
public float InputWindowStart;
public float InputWindowDuration;
public AttackType[] CancelableAttacks;
}
public ComboSequence[] comboSequence;
void Update() {
if (currentComboStep > 0)
{
comboResetTimer -= Time.deltaTime;
if (comboResetTimer <= 0)
{
ResetCombo();
}
}
}
public void TryNextAttack(AttackType inputType)
{
if (!canAcceptNextInput) return;
var currentStep = comboSequence[currentComboStep];
if (Array.Exists(currentStep.CancelableAttacks, t => t == inputType))
{
ExecuteAttack(inputType);
}
可以简单实现招式的派生和组合,只是单纯的连段不会有好的操作体验,要有相对克制的预输入才能在游玩体验上更流畅和有章法,如:
private Queue<BufferedInput> _inputQueue = new Queue<BufferedInput>();
private float _bufferWindow = 0.15f;
void Update()
{
ProcessExpiredInputs();
}
public void BufferInput(InputType type)
{
_inputQueue.Enqueue(new BufferedInput
{
type = type,
expireTime = Time.time + _bufferWindow
});
}
//相对克制预输入队列,不将所有的预输入内容都输出
public bool TryConsumeInput(InputType type)
{
foreach (var input in _inputQueue)
{
if (input.type == type && Time.time <= input.expireTime)
{
_inputQueue.Clear();
return true;
}
}
return false;
}
private void ProcessExpiredInputs()
{
while (_inputQueue.Count > 0 && _inputQueue.Peek().expireTime < Time.time)
{
_inputQueue.Dequeue();
}
}
4. 使用动画事件来驱动战斗,以实现对战斗节奏的控制,如:
private Dictionary<string, AnimationEvent[]> _cachedEvents = new Dictionary<string, AnimationEvent[]>();
public void PrecacheAnimationEvents(Animator animator)
{
foreach (var clip in animator.runtimeAnimatorController.animationClips)
{
_cachedEvents[clip.name] = clip.events;
clip.events = OptimizeEvents(clip.events);
}
}
private AnimationEvent[] OptimizeEvents(AnimationEvent[] original)
{
List<AnimationEvent> optimized = new List<AnimationEvent>();
foreach (var evt in original)
{
if (evt.functionName.StartsWith("Combat/"))
{
optimized.Add(evt);
}
}
return optimized.ToArray();
}
//通过动画事件驱动战斗分发
public void DispatchAnimationEvent(string eventPath)
{
var parts = eventPath.Split('/');
string systemName = parts[1];
string methodName = parts[2];
switch (systemName)
{
case "HitBox":
CombatSystem.Instance.HitBoxSystem.Invoke(methodName, 0);
break;
case "Camera":
CameraSystem.Instance.Invoke(methodName, 0);
break;
}
}
二、地图系统的实现
1. 设计相关
地图构成上相对简单,即据点地图为固定地图,关卡内地图部分区域随机生成
2. 实现相关
地图系统的实现上有几个要点:
- 以网格为基础,构建网格、世界坐标相互转换的坐标体系,以实现单纯离散网格能与连续空间对应
- 以数据驱动网格内容设计,如
- 是否可以通过、草地雪地土地等几乎不会变化的基础信息,可以直接预先生成
- 天气对地形有影响、解密或奖励机关的状态改变对地形有影响等相对低频次触发的变化信息,可以较低频率的同步
- 角色等对象占用或有临时出现的障碍地形之类相对高频触发的变化信息,可以直接使用事件驱动
- 通过数据结构化网格以实现动态网格加载内容,以确保场景无关性
- 导航系统
- 优化相关
1. 简单构建双向转换坐标系,如:
public Vector3 GridToWorld(int x, int y)
{
return new Vector3
(
x * cellSize + cellSize/2,
0,
y * cellSize + cellSize/2
);
}
public Vector2Int WorldToGrid(Vector3 pos)
{
return new Vector2Int
(
Mathf.FloorToInt(pos.x / cellSize),
Mathf.FloorToInt(pos.z / cellSize)
);
}
2. 导航系统可以优先检测碰撞体再使用网格导航,如:
//分帧处理
private IEnumerator CalculateWalkableSurface()
{
const float cellResolution = 0.5f;
int gridDimension = (int)(gridSize / cellResolution);
for (int y = 0; y < rowCount * gridDimension; y++)
{
for (int x = 0; x < columnCount * gridDimension; x++)
{
Vector3 scanOrigin = transform.position +
new Vector3(x * cellResolution, -10, y * -cellResolution);
bool isObstructed = Physics.CheckSphere(scanOrigin,
collisionCheckRadius, obstacleLayer);
if (!isObstructed && NavMesh.SamplePosition(scanOrigin,
out NavMeshHit hit, sampleDistance, walkableAreaMask))
{
Debug.DrawLine(hit.position, hit.position + Vector3.up,
Color.green, visualizationDuration);
}
}
yield return null;
}
}
3. 优化策略相对比较多
- 可以给区域加权使其优先加载,以此实现例如只加载方圆5km内的区域
- 对资源的生命周期追踪管理及时卸载
- 可以通过一些预见性或数据,优先保证某些区域的加载,如:
public void AdjustLoadingOrder(ScenePartition[] partitions)
{
Array.Sort(partitions, (a,b) =>
{
float priorityA = CalculatePriority(a);
float priorityB = CalculatePriority(b);
return priorityB.CompareTo(priorityA);
});
}
private float CalculatePriority(ScenePartition partition)
{
// 基于可见性、玩家位置、历史进入数据、玩家行为预测等优先加载
return ...;
}
三、NPC系统的实现
1. 设计相关
NPC在据点中,在进攻事件发生前主要以加速建造功能为主,在进攻事件发生后,则会参与战斗。在关卡内刷新随机的NPC,发现后可以加入据点。
2. 实现相关
NPC系统的实现更多是AI架构的实现,测试过后相对好的方案是使用FSM状态机来进行宏观状态的维护,例如NPC正在战斗/建造/休息等,实际战斗内具体的动作行为,再通过Behavior Tree进行处理,节点设计例如:
Selector
技能释放
Sequence
连段派生
Parallel
锁定目标并向其移动
Decorator
概率触发的特技或持续检测冷却时间
可以使用封装好的RVO算法做NPC的导航避障。
四、技能系统的实现
1. 设计相关
技能相关的部分为了可视化编辑,可以考虑使用ScriptableObject进行制作,尽量使技能系统既能数据驱动也能事件驱动,有相对可观且便捷的扩展性
2. 实现相关
1. 需要将技能系统数据结构化,以实现多样需求,如:
public class SkillData : ScriptableObject
{
public string skillName; // 技能名称
public float cooldown = 1f; // 冷却时间
public Sprite icon; // 技能图标
public List<SkillPhase> phases = new List<SkillPhase>(); // 技能阶段
public GameObject castVFX; // 施法特效
public AudioClip castSFX; // 施法音效
}
// 技能阶段配置
public class SkillPhase
{
[Range(0, 5)] public float triggerTime; // 触发时间(秒)
public SkillEffectType effectType; // 效果类型
public TargetSelectionStrategy targetStrategy; //目标选择方式
public EffectParams parameters; // 效果参数
}
2. 实际释放技能时再根据技能数据分别处理,如:
public class SkillSystem : MonoBehaviour
{
private SkillData currentSkill; // 当前释放的技能
private float cooldownTimer; // 冷却计时器
private Coroutine castingCoroutine;
// 开始释放
public void StartCast(SkillData skill)
{
if(cooldownTimer > 0 || currentSkill != null) return;
currentSkill = skill;
castingCoroutine = StartCoroutine(CastingProcess());
}
// 施法过程
private IEnumerator CastingProcess()
{
PlayCastAnimation();
SpawnVFX(currentSkill.castVFX);
PlaySFX(currentSkill.castSFX);
// 执行技能阶段
foreach(var phase in currentSkill.phases)
{
yield return new WaitForSeconds(phase.triggerTime);
// 获取目标
var targets = SelectTargets(phase.targetStrategy);
// 执行
ExecuteEffect(phase.effectType, targets, phase.parameters);
}
currentSkill = null;
cooldownTimer = currentSkill.cooldown;
}
// 目标选择方式
private List<GameObject> SelectTargets(TargetSelectionStrategy strategy)
{
// 比如圆形范围选择)
switch(strategy)
{
case TargetSelectionStrategy.Self:
return new List<GameObject>{gameObject};
case TargetSelectionStrategy.CircleRange:
return Physics.OverlapSphere(transform.position, 5f)
.Select(c => c.gameObject).ToList();
default:
return new List<GameObject>();
}
}
3. 通过不同的效果参数配置,来实现不同的如前摇后摇持续等技能效果,如:
public interface ISkillEffect
{
void Execute(GameObject caster, List<GameObject> targets, EffectParams parameters);
}
// 伤害效果实现
public class DamageEffect : ISkillEffect
{
public void Execute(GameObject caster, List<GameObject> targets, EffectParams parameters)
{
foreach(var target in targets)
{
if(target.TryGetComponent<HealthSystem>(out var health))
{
float finalDamage = parameters.baseValue +
(parameters.isPercentDamage ?
health.maxHP * parameters.percentValue : 0);
//造成伤害
}
}
}
}
}
三、资源管理方案
如何使用Addressable完成第三点资源管理方案
最新发布