Unity plyGame插件技能模块分析

plyGame 插件

plyGame 是一款Unity游戏引擎的视觉游戏开发工具。它可以让开发者不必编程就可以创建游戏原型,同时仍然允许以脚本方式来与系统API进行交互。

plyGame 出于易用性的考虑,提供了用来创建砍杀类RPG游戏的组件和编辑器。

这里仅分析其技能模块。

Skill 技能模块

Basic 基本信息

  • Execution Time 执行时间,这段时间内用来播放动作等,不能释放新技能
  • Cooldown Time 冷却时间,下一次能使用的间隔时间
  • Perform while move 是否可以移动的时候释放技能
  • Force Stop 是否强制停止移动来释放技能,若未设置,则移动时候不能释放技能,当Perform while move未设置时有效
  • Can be queued 是否允许放到队列,否则的话,只有当前没有使用其他技能的时候才会生效
    • Auto Queue 自动放到队列,仅当Can be queued选项开启时有效
  • Actor must face target 必须朝向目标

Activation 技能激活类型

  • User 主动技能
  • Passive 被动技能

Delivery 技能生效类型

Instant 立即生效

如近战、治疗、远程非投射类型技能

  • Direction/ Location 技能执行的方向和位置
    • Actor Direction 角色当前朝向(方向技能)
    • Mouse Direction 鼠标所指的方向(方向技能)
    • Selected Direction 所选中目标的方向(方向技能)
    • At Click Position 所点击的位置(位置技能)
    • At Selected 目标被选中才能使用(位置技能)
    • Mouse Over 鼠标之下必须有目标(位置技能)
    • Camera Forward 摄像机的前进方向(方向技能)
    • Camera Direction 摄像机的XZ平面(方向技能)
    • Cross hairOver 通过射线来选择目标(位置技能)
  • Hit Height % 击中的高度偏移,0在脚底,50在中间,100在头顶
  • Hit Delay 击中事件延迟,若为0,则一收集到目标就触发击中事件
  • Max Targets Select 最大的目标收集数量
  • Max Distance from self 技能影响的最大距离(米)
  • Targteting Range 方向技能收集目标的角度,位置技能收集目标的半径

Projectile 投射生效

创建投射物来击中目标

  • Prefab(s) 投射物预制
  • Move Method 投射物的方向和移动方法
    • ActorFaceDirection 笔直地从角色发射出去
    • AngledFromEach 成角度的发射,不止一个投射物,如多重箭
      • Random Angle 是否设置随机角度,若否的话,则根据Targeting Range来设置角度
    • DirectToTarget 投射物寻找到目标,然后朝着目标移动
      • Follow 是否跟随,若不设置,则投射物只会移动到投射物创建时目标的位置
      • Hit Height % 击中的高度偏移,0在脚底,50在中间,100在头顶
  • Max Projectiles 最大投射物的创建数量
  • Create Delay 创建第一个投射物的延迟,使用这个来跟动画同步
  • Between Create Delay 创建下一个投射物的延迟
  • Use Vector offset 投射物创建时的偏移
    • Create at Offset 相对角色本身的坐标偏移
    • Create at Tagged 角色的虚拟体,以此虚拟体坐标来创建
  • Move Speed 投射物的移动速度
  • Collision Ray Width 射线碰撞检测宽度,若为0,则为直线检测,否则为球形检测
  • Fizzle Distance 失败的距离,投射物最大的飞行距离。DirectToTarget移动方法的话,则没有这个选项
  • Max Live Time 最大生存时间,通常使用这个选项或者Fizzle Distance选项来控制失败
  • Trigger secondary on Fizzle 失败的时候,是否要触发二次效果
    • … only if obstacle 是否仅当由于碰撞到障碍物导致失败,才触发二次效果
  • Destroy Projectile on Hit 击中的时候销毁投射物,通常开启这个选项,除非像镭射光那种,穿过目标,并且对其后的目标也造成伤害,只有当时间到达或者距离到达的时候才会销毁投射物
  • Prevent Projectile UpDown 锁定投射物的Y轴
  • Actor Direction 技能执行的方向和位置
    • Actor Direction 角色当前朝向(方向技能)
    • Mouse Direction 鼠标所指的方向(方向技能)
    • Selected Direction 所选中目标的方向(方向技能)
    • At Click Position 所点击的位置(位置技能)
    • At Selected 目标被选中才能使用(位置技能)
    • Mouse Over 鼠标之下必须有目标(位置技能)
    • Camera Forward 摄像机的前进方向(方向技能)
    • Camera Direction 摄像机的XZ平面(方向技能)
    • Cross hairOver 通过射线来选择目标(位置技能)
  • Max Distance from self 技能影响的最大距离(米)
  • Targteting Range 方向技能收集目标的角度,位置技能收集目标的半径
Custom projectile logic 自定义投射物逻辑

当一个投射物创建的时候,将会添加SkillProjectile脚本组件到投射物物体上,这个组件驱动投射物。当要自定义投射物逻辑的时候,派生重载SkillProjectile脚本。

Targeting Methods 目标搜集方法

  • Self 自身作为目标,如治疗技能;
  • Selected 所选择的目标;
  • Auto 根据技能范围来自动选择有效的目标;

Valid Target(s) 有效目标

  • Player 不管任何阵营;
  • Friendly Actor 友方阵营;
  • Neutral Actor 中立阵营;
  • Hostile Actor 敌对阵营;
  • RPG Object 任意对象;

ObstacleCheckMask 障碍物检测

检测技能释放者与目标之间是否有障碍物,使用 Layer 掩码来检测。

Secondary Hit 二次击中效果

当一个效果击中目标时,可以引起触发第二次效果。这第二次效果将会收集第一次效果半径周围的目标。

  • Max Secondary Targets 最大二次效果目标数量,若为0,则表示关闭这个功能
  • Obstacle Check 障碍物检测
    • Check height offset 障碍物检测的高度
  • Range 二次效果半径

Skill Event 技能事件

  • On Skill Activate 当技能被激活的时候
  • On Skill Effect 当技能效果被创建的时候,对投射技能来说是投射物创建的时候,对立即技能来说是技能被激活并且有效执行的时候
  • On Skill Hit 当技能效果击中或影响目标的时候
  • On Skill Secondary Hit 当技能效果由于二次效果而击中或影响目标的时候
  • On Skill Fizzle 当投射技能由于超过生存时间或超过最大距离导致失败的时候
  • On Validate 在技能被激活之前进行验证,返回True才能真正激活技能

技能流程

角色类 Actor

  • executingSkill 当前正在执行的技能
  • queuedSkill 队列里等待被执行的技能
// 角色触发执行技能
public virtual void QueueSkillForExecution(Skill skill)
{
    QueueSkillForExecution(skill, false, null, Vector3.zero, Vector3.zero);
}

// 将一个技能放到队列里面准备执行
public virtual void QueueSkillForExecution(Skill skill, bool ifNoOtherQueued, 
    Targetable forceSelectedObject, Vector3 forceSelectedPosition, Vector3 forceMousePosition)
{
    if (skill == null) return;
    if (ifNoOtherQueued && queuedSkill != null) return;

    // 技能不能放到队列里面
    if (skill.canBeQueued == false)
    {    // 那么检查当前是否有正在执行的技能
        if (executingSkill != null)
        {
            // 有其他技能正在执行,那么就失败
            if (executingSkill.IsExecuting())
            {
                ClearQueuedSkill();
                return;
            }
        }

        // 技能还在冷却
        if (skill.CoolingDown()) return;

        // 不能放到队列,因为当前角色还在移动
        if (skill.forceStop == false && skill.mayPerformWhileMove == false)
        {
            if (character.Velocity().magnitude >= 0.01f) return;
        }
    }

    // 不确定有其他队列技能
    if (false == ifNoOtherQueued)
    {
        // ...... 省略,根据技能方向和位置
        // 求得queuedSkill_SelectedObject、queuedSkill_MousePosition

        if (queuedSkill_SelectedObject != null)
        {
            queuedSkill_TargetPosition = queuedSkill_SelectedObject.transform.position;
        }
        else
        {
            queuedSkill_TargetPosition = queuedSkill_MousePosition;
        }

        queuedSkill = skill;
    }
    else
    {
        queuedSkill = skill;
        queuedSkill_SelectedObject = forceSelectedObject;
        queuedSkill_MousePosition = forceMousePosition;
        queuedSkill_TargetPosition = forceSelectedPosition;
    }
}

protected void LateUpdate()
{
    // ... 检测是否死亡等

    // 当前已经有正在执行的技能
    if (executingSkill != null)
    {
        // 技能执行完毕
        if (false == executingSkill.IsExecuting())
        {
            // 重新开启角色移动
            character.hint_DoNotMove = false;
            executingSkill = null;
        }
    }

    else
    {
        // 有等待被执行的技能
        if (queuedSkill != null)
        {
            // 技能的选中目标和坐标
            if (queuedSkill_SelectedObject != null)
            {
                queuedSkill_TargetPosition = queuedSkill_SelectedObject.transform.position;
            }

            // 是否可以执行技能的判断
            if (CanPerformQueuedSkill())
            {
                // 如果不能在移动的时候执行技能,那么禁止角色移动
                if (false == queuedSkill.mayPerformWhileMove)
                {
                    character.hint_DoNotMove = true;
                    character.Stop();
                }

                executingSkill = queuedSkill;
                queuedSkill = null;
                // 真正执行技能
                executingSkill.Execute(queuedSkill_SelectedObject, 
                    queuedSkill_TargetPosition, queuedSkill_MousePosition);

                if (character.IsPlayer())
                {
                    if (executingSkill.targetLocation == Skill.TargetLocation.MouseOver 
                        || executingSkill.targetLocation == Skill.TargetLocation.CrosshairOver)
                    {
                        character.SelectTarget(queuedSkill_SelectedObject);
                    }
                }
            }
        }
    }
}

// 是否可以执行技能的判断
private bool CanPerformQueuedSkill()
{
    // 如果技能不能在移动的时候执行,或者不能强制停止移动
    if (false == queuedSkill.mayPerformWhileMove && false == queuedSkill.forceStop)
    {    // 那么就检查当前是否在移动,是的话就不允许技能执行
        if (character.Velocity().magnitude >= 0.01f)
        {
            ClearQueuedSkill();
            return false;
        }
    }

    // 技能是否不在CD冷却时间内
    if (queuedSkill.IsReady())
    {
        // 检查目标是否在技能范围内
        if (false == queuedSkill.DistanceAcceptable(character._tr.position, 
            queuedSkill_TargetPosition, queuedSkill_MousePosition, 
            out targetPositionForSkill, queuedSkill_SelectedObject))
        {
            // 如果不在范围内,那么走到范围内,再进行执行技能
            // 如果走不到,则丢弃这个技能不再执行
            if (false == character.RequestMoveTo(targetPositionForSkill, true))
            {
                queuedSkill = null;
                return false;
            }
        }

        else
        {
            // 如果必须朝向目标,那么检查角色是否面向目标
            if (queuedSkill.actorMustFaceTarget 
                && false == queuedSkill.FacingAcceptable(character._tr.forward, 
                    queuedSkill_TargetPosition, queuedSkill_MousePosition, out targetDirectionForSkill))
            {
                // 如果没有朝向目标,则转动方向
                // 如果转动失败,则丢弃这个技能不再执行
                if (false == character.RequestFaceDirection(targetDirectionForSkill, 
                    queuedSkill.executionTimeout > 0.1f ? queuedSkill.executionTimeout : 0.1f))
                {
                    queuedSkill = null;
                    return false;
                }
            }

            else
            {
                return true;
            }
        }
    }

    return false;
}

技能类 Skill

// 执行技能
public virtual void Execute(Targetable selectedObject, Vector3 selectedPosition, Vector3 mousePosition)
{
    // 调用验证事件
    if (eventHandler != null)
    {
        if (eventHandler.OnValidateSkill(owner, this) == false) return;
    }

    gameObject.SetActive(true);
    // 开始计时执行时间和冷却时间
    executeTimer = executionTimeout;
    cooldownTimer = cooldownTimeout;

    // 调用激活事件
    if (eventHandler != null)
    {
        eventHandler.OnActivate(owner, this, selectedPosition, mousePosition);
        owner.gameObject.BroadcastMessage("OnUsesSkill", this, SendMessageOptions.DontRequireReceiver);
    }
    // 允许放到队列,并且自动放到队列有效的时候,就会在当前没有队列技能的时候放到队列里面
    if (canBeQueued && autoQueue) 
        owner.QueueSkillForExecution(this, true, selectedObject, selectedPosition, mousePosition);

    // 有效的目标
    if (validTargetsMask != 0)
    {
        // 立即生效技能
        if (deliveryMethod == DeliveryMethod.Instant) 
            ExecuteInstant(selectedObject, selectedPosition, mousePosition);
        // 投射技能
        else if (deliveryMethod == DeliveryMethod.Projectile) 
            ExecuteProjectile(selectedObject, selectedPosition, mousePosition);
    }
}
投射技能
角色当前朝向投射
Vector3 forward = owner.transform.forward;
// ...... 根据目标方向和位置类型调整forward

// 最终坐标点
Vector3 tl = owner.transform.position + (forward * maxFlightDistance);
tl.y += projectileCreateOffset.y;

float waitTime = projectileCreateDelay;
for (int i = 0; i < maxEffects; i++)
{
    CreateProjectile(i, waitTime, forward, tl, null, Vector3.zero);
    waitTime += projectileCreateDelayBetween;
}
成角度的发射
Vector3 forward = owner.transform.forward;
// ...... 根据目标方向和位置类型调整forward

// 最大投射只有1个的情况
if (maxEffects == 1)
{
    Vector3 tl = owner.transform.position + (forward * maxFlightDistance);
    tl.y += projectileCreateOffset.y;
    CreateProjectile(0, 0f, forward, tl, null, Vector3.zero);
}
else
{
    // 每个投射所占的角度
    float angleUnit = targetingAngle / (maxEffects - 1);
    float maxAngle = (targetingAngle / 2);
    float minAngle = -maxAngle;
    float angle = minAngle;
    float waitTime = projectileCreateDelay;

    // 投射偏移
    Vector3 pos = owner.transform.position;
    pos += (owner.transform.right * projectileCreateOffset.x);
    pos += (owner.transform.up * projectileCreateOffset.y);
    pos += (owner.transform.forward * projectileCreateOffset.z);

    for (int i = 0; i < maxEffects; i++)
    {
        // 随机角度
        if (moveMethod_b_opt) angle = Random.Range(minAngle, maxAngle);
        Vector3 tl = pos + (Quaternion.AngleAxis(+angle, Vector3.up) * forward * maxFlightDistance);
        CreateProjectile(i, waitTime, forward, tl, null, Vector3.zero);
        waitTime += projectileCreateDelayBetween;
        angle += angleUnit;
    }
}
直达目标投射
// 收集目标
List<Targetable> targets = CollectTargetsBasedOnTargetingLocation(selectedObject, selectedPosition, mousePosition);

float waitTime = projectileCreateDelay;
if (targets.Count > 0)
{
    int projectilesleft = maxEffects;

    while (projectilesleft > 0)
    {
        // 击中的高度比例偏移
        float hitOffs = (float)hitHeightPercentage / 100f;
        for (int i = 0; i < targets.Count; i++)
        {
            if (targets[i] == null) continue;

            // 计算击中高度
            float y = 1f;
            Collider col = targets[i].GetComponent<Collider>();
            if (col != null) y = col.bounds.size.y;
            Vector3 followOffset = new Vector3(0f, y * hitOffs, 0f);

            // 跟随目标
            if (moveMethod_b_opt)
            {
                CreateProjectile(i, waitTime, owner.transform.forward, targets[i].transform.position, targets[i].transform, followOffset);
            }
            else
            {
                Vector3 pos = targets[i].transform.position + followOffset;
                CreateProjectile(i, waitTime, owner.transform.forward, pos, null, followOffset);
            }
            waitTime += projectileCreateDelayBetween;

            projectilesleft--;
            if (projectilesleft <= 0) break;
        }
    }

}
else
{
    // 没有目标的话,随机投射
    int leftToCreate = maxEffects;
    {
        Vector3 forward = owner.transform.forward;
        float maxAngle = (targetingAngle / 2);
        float minAngle = -maxAngle;
        float angle = minAngle;

        Vector3 pos = owner.transform.position;
        pos += (owner.transform.right * projectileCreateOffset.x);
        pos += (owner.transform.up * projectileCreateOffset.y);
        pos += (owner.transform.forward * projectileCreateOffset.z);

        for (int i = 0; i < leftToCreate; i++)
        {
            angle = Random.Range(minAngle, maxAngle);
            Vector3 tl = pos + (Quaternion.AngleAxis(+angle, Vector3.up) * forward * maxFlightDistance);
            CreateProjectile(i, waitTime, forward, tl, null, Vector3.zero);
            waitTime += projectileCreateDelayBetween;
        }
    }
}
SkillProjectile

每个投射物都会挂上这个脚本,在Update事件里来控制投射物的移动和触发事件。

protected void Update()
{
    if (GameGlobal.Paused) return;

    if (owner == null)
    {
        if (Skill.SkillProjectilePool != null) Skill.SkillProjectilePool.Destroy(gameObject);
        else Destroy(gameObject); // 拥有者无效的时候,则投射失败
        return;
    }

    if (owner.owner.IsDead())
    {
        if (Skill.SkillProjectilePool != null) Skill.SkillProjectilePool.Destroy(gameObject);
        else Destroy(gameObject); // 角色死亡的时候,也投射失败
        return;
    }

    // 跟踪直达目标的时候,目标坐标要一直更新
    if (targetTr != null) targetLocation = targetTr.position + followOffset;

    // 投射物当前的世界坐标
    prevPos = _tr.position;
    // 根据速度沿着目的地前进
    _tr.position = Vector3.MoveTowards(_tr.position, targetLocation, Time.deltaTime * moveSpeed);
    // 调整方向,对准目的地
    Vector3 f = (targetLocation - _tr.position).normalized;
    if (f != Vector3.zero) _tr.forward = f;

    // 构建射线
    ray.origin = prevPos;
    ray.direction = _tr.position - prevPos;
    distance = ray.direction.magnitude;

    if (collisionRayWidth > 0.0f)
    {
        // 球形射线检测目标
        if (Physics.SphereCast(ray, collisionRayWidth, out hit, distance, validTargetsLayerMask))
        {
            if (owner != null)
            {
                // 还未包含到击中列表
                if (false == hitList.Contains(hit.transform.gameObject))
                {
                    // 尝试击中目标
                    if (owner.AttemptHit(hit.transform.gameObject, hit.point, gameObject))
                    {
                        // 击中即销毁投射物的话
                        if (destroyProjectileOnHit)
                        {
                            if (Skill.SkillProjectilePool != null) Skill.SkillProjectilePool.Destroy(gameObject);
                            else Destroy(gameObject);
                            return;
                        }
                        else
                        {
                            hitList.Add(hit.transform.gameObject);
                        }
                    }
                }
            }
            else
            {    // 拥有者无效的时候销毁投射物
                if (Skill.SkillProjectilePool != null) Skill.SkillProjectilePool.Destroy(gameObject);
                else Destroy(gameObject);
                return;
            }
        }


        // 检查是否碰到了障碍物
        if (obstacleCheckMask != 0)
        {
            if (Physics.SphereCast(ray, collisionRayWidth, out hit, distance, obstacleCheckMask))
            {
                // 是的话,则投射失败
                if (owner != null)
                {
                    owner.ExecuteFizzleEvent(_tr.position, true, gameObject);
                    if (triggerSecondaryOnFizzle) owner.ExecuteSecondary(_tr.position, null, gameObject);
                }

                if (Skill.SkillProjectilePool != null) Skill.SkillProjectilePool.Destroy(gameObject);
                else Destroy(gameObject);
                return;
            }
        }
    }
    else
    {
        // 检查射线
        if (Physics.Raycast(ray, out hit, distance, validTargetsLayerMask))
        {
            if (owner != null)
            {
                if (false == hitList.Contains(hit.transform.gameObject))
                {
                    if (owner.AttemptHit(hit.transform.gameObject, hit.point, gameObject))
                    {
                        if (destroyProjectileOnHit)
                        {
                            if (Skill.SkillProjectilePool != null) Skill.SkillProjectilePool.Destroy(gameObject);
                            else Destroy(gameObject);
                            return;
                        }
                        else
                        {
                            hitList.Add(hit.transform.gameObject);
                        }
                    }
                }
            }
            else
            {
                if (Skill.SkillProjectilePool != null) Skill.SkillProjectilePool.Destroy(gameObject);
                else Destroy(gameObject);
                return;
            }
        }


        if (obstacleCheckMask != 0)
        {
            if (Physics.Raycast(ray, out hit, distance, obstacleCheckMask))
            {
                if (owner != null)
                {
                    owner.ExecuteFizzleEvent(_tr.position, true, gameObject);
                    if (triggerSecondaryOnFizzle) owner.ExecuteSecondary(_tr.position, null, gameObject);
                }

                if (Skill.SkillProjectilePool != null) Skill.SkillProjectilePool.Destroy(gameObject);
                else Destroy(gameObject);
                return;
            }
        }
    }

    // 当已经到达目的地的时候,或者时间已经到了,则投射失败
    maxLiveTime -= Time.deltaTime;
    if ((targetLocation - _tr.position).sqrMagnitude < 0.01f || maxLiveTime <= 0.0f)
    {
        if (owner != null)
        {
            owner.ExecuteFizzleEvent(_tr.position, false, gameObject);
            if (triggerSecondaryOnFizzle && triggerSecondaryOnFizzleOnlyIfObstacle == false)
            {
                owner.ExecuteSecondary(_tr.position, null, gameObject);
            }
        }

        if (Skill.SkillProjectilePool != null) Skill.SkillProjectilePool.Destroy(gameObject);
        else Destroy(gameObject);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值