ddl战士 ———— α冲刺Day2

ddl战士 2025-10-31 21:57:55
这个作业属于哪个课程2501_CS_SE_FZU
这个作业要求在哪里团队作业——站立式会议+α冲刺
这个作业的目标α冲刺Day2
其他参考文献《构建之法》、Google Style Guides

1. 站立式会议照片

img

2. 项目燃尽图

img

3. 成员工作总结

成员角色昨日至今日日站立会安排存在的问题 / 遇到的困难心得体会今日至明日站立会安排
陈志豪(敌人模块)1. 完成敌人死亡处理(任务 E-001),实现 AI 禁用、死亡动画与资源清理;2. 集成 EnemyDrop 系统(任务 E-002),死亡时可触发 LootTable 生成掉落物。1. 多敌人同时死亡时,掉落物重叠导致拾取判定混乱;2. 敌人击退抗性参数(任务 E-003)调试未达预期,击退距离过短。多对象并发处理需考虑碰撞体层级与空间分布,避免资源竞争或交互冲突,参数调试需结合实际玩法场景验证。1. 优化掉落物生成位置,添加随机偏移(5f 范围内);2. 调整击退抗性公式,测试并确定合理参数;3. 编写敌人 AI 行为优化(E-004)的基础逻辑。
胡定赟 (UI/UX 设计)1. 完成血条系统集成(任务 U-001),绑定 Attribute.OnHealthChanged 事件,实现实时更新;2. 优化背包 UI 基础布局(任务 U-005),调整物品槽位间距与 Icon 显示比例。1. 血条在玩家快速受击时出现数值跳变,平滑过渡效果未生效;2. 背包 UI 在低分辨率(720p)下槽位显示不全,适配未覆盖小屏设备。UI 适配需覆盖多分辨率场景,动态数值更新需考虑视觉流畅度,避免因数据频繁变化导致的用户体验下降。1. 为血条添加数值平滑过渡算法(Lerp 插值);2. 优化 UI 适配锚点,确保 720p-2K 分辨率下正常显示;3. 开发死亡 UI(任务 U-006)的基础框架。
阮航宇 (地图与资源设计)1. 完成 Tilemap 地图完整构建(任务 M-005),使用 cave_tileset 补充场景细节(洞穴通道、障碍物);2. 实现地图可交互物(宝箱)逻辑(任务 M-006),点击后触发掉落。1. 部分 Tile 碰撞层未正确设置,导致玩家可穿透墙壁;2. 宝箱打开动画与掉落物生成存在 1 秒延迟,交互反馈不及时。地图设计需兼顾视觉效果与玩法逻辑,碰撞层配置需逐一校验,避免因细节遗漏导致的玩法漏洞。1. 批量检查并修正 Tile 碰撞层(Ground 层设为不可穿透);2. 优化宝箱交互逻辑,同步动画与掉落物生成;3. 添加地图装饰元素(任务 M-008),如岩石、火把。
张天荣 (攻击模块)1. 完成 Attack 组件与 PlayerController 集成(任务 P-002),实现 J 键触发近战攻击;2. 优化攻击判定(任务 C-001),调整攻击盒大小(半径 0.3f→0.4f)与命中特效触发时机;3. 实现伤害数字显示(任务 C-004),添加浮动文字与暴击红色高亮效果1. 快速连续按 J 键时,攻击动作叠加导致动画播放混乱;2. 攻击盒偶尔未检测到近距离敌人,判定范围存在偏差;3. 暴击伤害数字与普通伤害数字重叠,视觉区分度不足。攻击系统需兼顾 “手感” 与 “准确性”,动作与判定的同步是核心,需通过冷却锁、范围补全等细节优化提升体验;视觉反馈需明确区分不同状态(普通 / 暴击),帮助玩家快速感知战斗效果。1. 添加攻击冷却锁(0.5 秒),避免动作叠加;2. 优化攻击盒检测逻辑,增加 SphereCast 范围检测补全判定;3. 调整暴击数字字号(普通 14 号→暴击 16 号)与偏移量(向上偏移 50px);4. 集成元素攻击效果(火 / 冰)与攻击判定联动(任务 C-002)。
汪涛(测试与性能优化、玩家模块)1. 完成玩家死亡状态处理(任务 P-001),实现禁用控制、死亡动画播放与重生机制;2. 优化移动与跳跃手感(任务 P-004),调整跳跃力与移动加速度参数。3. 执行全系统集成测试(任务 T-001),覆盖 “攻击 - 受击 - 死亡 - 掉落 - 拾取” 流程;4. 定位 2 处性能瓶颈:Update () 中频繁调用 FindObjectOfType、技能释放时 GC 峰值过高。1. 重生后偶现物理碰撞失效,需重新激活 Rigidbody2D 组件;2. 死亡动画与重生逻辑衔接存在 0.5 秒卡顿。物理组件状态管理需更细致,重生时需逐一校验依赖组件的激活状态,避免因组件未初始化导致的异常;测试需提前确认资源加载状态,性能优化需优先解决高频调用与 GC 问题,确保目标设备的流畅运行。1. 修复重生后物理碰撞失效问题;2. 完成玩家受击处理(任务 P-008),添加击退效果与无敌帧;3. 提交代码并同步至主分支。
莫馥玮(技能模块)1. 完成 Fireball 投射体系统(任务 S-001),实现投射、碰撞检测与伤害触发;2. 开发技能冷却 UI(任务 S-003),绑定技能 CD 与冷却环动画。1. Fireball 投射体飞行时穿透敌人,碰撞层 Mask 设置错误;2. 技能冷却环动画与实际 CD 不同步,需优化事件监听逻辑。投射体系统需严格控制碰撞层级,避免误触发非目标对象;UI 与逻辑的同步依赖事件总线,需确保事件发送与接收的时序一致性。1. 修正投射体碰撞层 Mask,仅检测 Enemy 层;2. 完成近战技能 DashStab(任务 S-002)的位移与伤害判定;3. 同步技能 UI 与逻辑的事件绑定(任务 S-004)。

4.成果证明

UI:

using UnityEngine;
using UnityEngine.SceneManagement;

public class SimpleSceneLoader : MonoBehaviour
{
    public void LoadSampleScene()
    {
        // 直接尝试加载SampleScene
        SceneManager.LoadScene("Main Scenes");
    }

    // 备用方法:按索引加载
    public void LoadSceneByIndex(int sceneIndex)
    {
        SceneManager.LoadScene(sceneIndex);
    }
}

using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class QuitGameManager : MonoBehaviour
{
    [Header("退出按钮")]
    public Button quitButton;

    void Start()
    {
        // 如果没有指定按钮,自动查找
        if (quitButton == null)
        {
            quitButton = GameObject.Find("退出游戏Button")?.GetComponent<Button>();
            // 或者根据文字查找
            if (quitButton == null)
            {
                Button[] allButtons = FindObjectsOfType<Button>();
                foreach (Button btn in allButtons)
                {
                    TextMeshProUGUI btnText = btn.GetComponentInChildren<TextMeshProUGUI>();
                    if (btnText != null && btnText.text == "退出游戏")
                    {
                        quitButton = btn;
                        break;
                    }
                }
            }
        }

        // 绑定点击事件
        if (quitButton != null)
        {
            quitButton.onClick.AddListener(QuitGame);
            Debug.Log("退出按钮事件绑定成功");
        }
        else
        {
            Debug.LogError("没有找到退出游戏按钮!");
        }
    }

    public void QuitGame()
    {
        Debug.Log("退出游戏按钮被点击");

#if UNITY_EDITOR
        // 在编辑器中停止播放
        UnityEditor.EditorApplication.isPlaying = false;
        Debug.Log("编辑器模式:停止运行");
#else
            // 在打包后的游戏中退出程序
            Application.Quit();
            Debug.Log("应用程序:退出游戏");
#endif
    }
}

img

敌人模块:

using UnityEngine;
using System.Collections;

public class EnemyAI : MonoBehaviour
{
    [Header("移动参数")]
    public float moveSpeed = 3f;
    public float chaseSpeed = 4.5f;
    public float wallCheckDistance = 0.2f;
    public LayerMask wallLayer;

    [Header("检测与攻击参数")]
    public float attackRange = 2f;
    public float attackDelay = 0.5f;
    public int attackDamage = 10;

    [Header("检测点设置")]
    public Transform detectionPoint;
    public Transform attackPoint;
    public Transform wallCheckLeft;
    public Transform wallCheckRight;

    [Header("玩家设置")]
    public Transform player;

    [Header("检测区域形状 (椭圆)")]
    public float detectionWidth = 10f;
    public float detectionHeight = 5f;
    public LayerMask playerLayer;

    [Header("击退参数")]
    public float windKnockbackDuration = 0.3f;
    public float windKnockbackForce = 8f;

    private Rigidbody2D rb;
    private bool isAttacking = false;
    private bool isChasing = false;
    private bool facingRight = true;
    private bool hitWall = false;
    [HideInInspector] public bool isKnockedBack = false;
    private float flipThreshold = 0.5f;

    private SpriteRenderer sprite; // ✅ 专门用于控制翻转外观

    private void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        sprite = GetComponentInChildren<SpriteRenderer>();

        rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
        rb.freezeRotation = true;

        if (player == null)
        {
            player = GameObject.FindWithTag("Player")?.transform;
            if (player == null)
                Debug.LogError("⚠ 找不到标记为 'Player' 的对象!");
        }
    }

    private void Update()
    {
        if (isAttacking || isKnockedBack) return;

        bool playerDetected = IsPlayerInDetectionRange();

        if (playerDetected && !isChasing)
        {
            isChasing = true;
            Debug.Log("🎯 玩家进入检测范围,开始追击!");
        }
        else if (!playerDetected && isChasing)
        {
            isChasing = false;
            Debug.Log("🚶 玩家离开检测范围,恢复巡逻");
        }

        if (playerDetected)
        {
            float xDiff = player.position.x - transform.position.x;
            bool playerOnRight = xDiff > 0;
            if (playerOnRight != facingRight)
                Flip(playerOnRight);
        }

        if (IsPlayerInAttackRange() && !isAttacking)
        {
            StartCoroutine(AttackPlayer());
        }
    }

    private void FixedUpdate()
    {
        if (isAttacking)
        {
            rb.velocity = Vector2.zero;
            return;
        }

        if (isKnockedBack)
            return;

        CheckWall();

        if (isChasing && player != null)
        {
            ChasePlayer();
        }
        else
        {
            Patrol();
        }

        // ✅ 新增:防止嵌入墙体的安全检测
        if (!isKnockedBack && !isAttacking)
        {
            CheckAndFixWallEmbedding();
        }
    }

    void Patrol()
    {
        if (hitWall)
        {
            Flip(!facingRight);
            hitWall = false;
        }

        float moveDir = facingRight ? 1f : -1f;
        rb.velocity = new Vector2(moveDir * moveSpeed, rb.velocity.y);
    }

    void ChasePlayer()
    {
        if (player == null) return;

        float xDiff = player.position.x - transform.position.x;

        if (Mathf.Abs(xDiff) > flipThreshold)
        {
            bool shouldFaceRight = xDiff > 0;
            if (shouldFaceRight != facingRight)
                Flip(shouldFaceRight);
        }

        if (Mathf.Abs(xDiff) < 3f)
        {
            rb.velocity = new Vector2(0, rb.velocity.y);
            return;
        }

        // ✅ 根据朝向选择检测点
        Transform checkPoint = facingRight ? wallCheckRight : wallCheckLeft;
        Vector2 dir = facingRight ? Vector2.right : Vector2.left;

        RaycastHit2D wallHit = Physics2D.Raycast(checkPoint.position, dir, wallCheckDistance, wallLayer);
        bool blocked = wallHit.collider != null;

        if (blocked)
        {
            rb.velocity = new Vector2(0, rb.velocity.y);
            Debug.Log("🧱 追击时检测到墙体,停止前进");
        }
        else
        {
            float moveDir = Mathf.Sign(xDiff);
            rb.velocity = new Vector2(moveDir * chaseSpeed, rb.velocity.y);
        }
    }

    // ✅ 翻转视觉外观,而不是整体缩放
    void Flip(bool faceRight)
    {
        facingRight = faceRight;

        // ✅ 只翻转Sprite,不改变Transform坐标系
        if (sprite != null)
            sprite.flipX = !faceRight;
    }

    // ✅ 检查墙体(巡逻用)
    void CheckWall()
    {
        Transform checkPoint = facingRight ? wallCheckRight : wallCheckLeft;
        if (checkPoint == null) return;

        Vector2 dir = facingRight ? Vector2.right : Vector2.left;
        RaycastHit2D hit = Physics2D.Raycast(checkPoint.position, dir, wallCheckDistance, wallLayer);
        hitWall = hit.collider != null && !isChasing;
    }

    IEnumerator AttackPlayer()
    {
        isAttacking = true;
        rb.velocity = Vector2.zero;
        Debug.Log("⚔ 敌人发动攻击!");

        yield return new WaitForSeconds(attackDelay / 2f);

        if (IsPlayerInAttackRange())
        {
            Attribute playerAttr = player.GetComponent<Attribute>();
            if (playerAttr != null)
                playerAttr.TakeDamage(attackDamage);
            Debug.Log($"💥 攻击命中,造成 {attackDamage} 伤害!");
        }

        yield return new WaitForSeconds(attackDelay / 2f);
        isAttacking = false;
    }

    bool IsPlayerInDetectionRange()
    {
        if (player == null || detectionPoint == null) return false;

        Vector2 offset = player.position - detectionPoint.position;
        float ellipseValue =
            (offset.x * offset.x) / (detectionWidth * detectionWidth / 4f) +
            (offset.y * offset.y) / (detectionHeight * detectionHeight / 4f);

        return ellipseValue <= 1f;
    }

    bool IsPlayerInAttackRange()
    {
        Collider2D[] hits = Physics2D.OverlapCircleAll(attackPoint.position, attackRange, playerLayer);
        return hits.Length > 0;
    }

    public void ApplyWindKnockback(float force, bool fromRight)
    {
        if (isKnockedBack) return;
        StartCoroutine(KnockbackCoroutine(force, fromRight));
    }

    IEnumerator KnockbackCoroutine(float force, bool fromRight)
    {
        isKnockedBack = true;
        isAttacking = false;
        isChasing = false;

        float dir = fromRight ? 1f : -1f;
        float originalY = transform.position.y;
        float elapsed = 0f;

        float knockbackSpeed = force / windKnockbackDuration;

        Debug.Log($"🌀 击退开始:方向={(fromRight ? "右" : "左")}, 力量={force}");

        while (elapsed < windKnockbackDuration)
        {
            elapsed += Time.deltaTime;

            float moveStep = knockbackSpeed * Time.deltaTime;
            Vector2 moveDir = new Vector2(dir, 0f);

            // ✅ 改进:使用多个检测点进行更可靠的墙体检测
            bool willHitWall = CheckWallInKnockbackDirection(dir, moveStep);

            if (willHitWall)
            {
                // 🧱 如果会撞到墙体,调整位置到安全距离
                float safeDistance = FindSafeKnockbackDistance(dir, moveStep);
                if (safeDistance > 0.01f)
                {
                    transform.position = new Vector3(
                        transform.position.x + dir * safeDistance,
                        originalY,
                        transform.position.z
                    );
                }
                Debug.Log("🧱 击退中检测到墙体,停止击退");
                break;
            }

            // ✅ 正常移动
            transform.position = new Vector3(
                transform.position.x + dir * moveStep,
                originalY,
                transform.position.z
            );

            yield return null;
        }

        rb.velocity = Vector2.zero;
        isKnockedBack = false;
        Debug.Log("✅ 击退结束");
    }

    // ✅ 新增:改进的墙体检测方法(多检测点)
    private bool CheckWallInKnockbackDirection(float direction, float distance)
    {
        if (wallCheckLeft == null || wallCheckRight == null) return false;

        // 使用多个检测点提高检测精度
        Vector2[] checkPoints = GetKnockbackCheckPoints(direction);
        Vector2 rayDir = new Vector2(direction, 0f);

        foreach (Vector2 point in checkPoints)
        {
            RaycastHit2D hit = Physics2D.Raycast(point, rayDir, distance + 0.1f, wallLayer);
            if (hit.collider != null)
            {
                return true;
            }
        }

        return false;
    }

    // ✅ 新增:获取击退检测点数组
    private Vector2[] GetKnockbackCheckPoints(float direction)
    {
        Transform primaryCheck = direction > 0 ? wallCheckRight : wallCheckLeft;
        Vector2 basePoint = primaryCheck.position;

        // 在垂直方向上创建多个检测点
        return new Vector2[]
        {
            basePoint,
            basePoint + Vector2.up * 0.5f,    // 上方检测点
            basePoint + Vector2.down * 0.5f,  // 下方检测点
            basePoint + Vector2.up * 0.25f,   // 中上检测点
            basePoint + Vector2.down * 0.25f  // 中下检测点
        };
    }

    // ✅ 新增:找到安全的击退距离
    private float FindSafeKnockbackDistance(float direction, float maxDistance)
    {
        if (wallCheckLeft == null || wallCheckRight == null) return 0f;

        Transform primaryCheck = direction > 0 ? wallCheckRight : wallCheckLeft;
        Vector2 rayDir = new Vector2(direction, 0f);

        // 找到最近的墙体距离
        float minDistance = maxDistance;
        Vector2[] checkPoints = GetKnockbackCheckPoints(direction);

        foreach (Vector2 point in checkPoints)
        {
            RaycastHit2D hit = Physics2D.Raycast(point, rayDir, maxDistance + 0.1f, wallLayer);
            if (hit.collider != null && hit.distance < minDistance)
            {
                minDistance = hit.distance;
            }
        }

        // 返回安全距离(留出0.05f的缓冲)
        return Mathf.Max(0, minDistance - 0.05f);
    }

    // ✅ 新增:检测并修复嵌入墙体的情况
    private void CheckAndFixWallEmbedding()
    {
        Collider2D[] overlappingWalls = Physics2D.OverlapCircleAll(transform.position, 0.3f, wallLayer);
        if (overlappingWalls.Length > 0)
        {
            Debug.LogWarning($"⚠️ 检测到 {name} 嵌入墙体,尝试修复");

            // 尝试向相反方向移动来脱离墙体
            Vector2 escapeDirection = facingRight ? Vector2.left : Vector2.right;
            RaycastHit2D hit = Physics2D.Raycast(transform.position, escapeDirection, 2f, ~wallLayer);

            if (hit.collider != null && ((1 << hit.collider.gameObject.layer) & wallLayer) == 0)
            {
                // 找到安全位置,移动过去
                transform.position = hit.point - (Vector2)escapeDirection * 0.1f;
                Debug.Log($"✅ 已修复 {name} 的墙体嵌入问题");
            }
        }
    }

    private void OnDrawGizmos()
    {
        if (detectionPoint != null)
        {
            Gizmos.color = new Color(0f, 0f, 1f, 0.3f);
            DrawEllipseGizmo(detectionPoint.position, detectionWidth, detectionHeight, 64);
        }

        if (attackPoint != null)
        {
            Gizmos.color = Color.red;
            Gizmos.DrawWireSphere(attackPoint.position, attackRange);
        }

        if (wallCheckLeft != null)
        {
            Gizmos.color = Color.yellow;
            Gizmos.DrawLine(
                wallCheckLeft.position,
                wallCheckLeft.position + Vector3.left * wallCheckDistance
            );
        }

        if (wallCheckRight != null)
        {
            Gizmos.color = Color.cyan;
            Gizmos.DrawLine(
                wallCheckRight.position,
                wallCheckRight.position + Vector3.right * wallCheckDistance
            );
        }

        // ✅ 新增:绘制击退检测点
        if (Application.isPlaying && isKnockedBack)
        {
            Gizmos.color = Color.magenta;
            Vector2[] checkPoints = GetKnockbackCheckPoints(facingRight ? 1f : -1f);
            foreach (Vector2 point in checkPoints)
            {
                Gizmos.DrawWireSphere(point, 0.1f);
            }
        }
    }

    void DrawEllipseGizmo(Vector3 center, float width, float height, int segments)
    {
        float a = width / 2f;
        float b = height / 2f;

        Vector3 prev = center + new Vector3(a, 0, 0);
        for (int i = 1; i <= segments; i++)
        {
            float angle = i * Mathf.PI * 2f / segments;
            float x = Mathf.Cos(angle) * a;
            float y = Mathf.Sin(angle) * b;
            Vector3 next = center + new Vector3(x, y, 0);
            Gizmos.DrawLine(prev, next);
            prev = next;
        }
    }
}

img

地图模块:

img

角色模块:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour, SM_ICharacterProvider, SM_IDamageable
{

    [Header("Components")]
    public Rigidbody2D rb;
    public Collider2D bodyCollider;
    
    [Header("Skill System")]
    public SM_SkillSystem skillSystem;  // 技能系统组件
    public Transform aimOrigin;         // 瞄准起点(通常是角色中心或武器位置)

    [Header("Collision Detection Settings")]
    [SerializeField] private bool useContinuousCollisionDetection = true;
    [SerializeField] private int groundCheckRays = 5; // 地面检测射线数量
    [SerializeField] private int wallCheckRays = 3;   // 墙壁检测射线数量
    [SerializeField] private float collisionPredictionTime = 0.1f; // 碰撞预测时间
    [SerializeField] private float maxSafeSpeed = 8f; // 最大安全速度,超过此速度可能穿模

    // 移动功能的参数
    [Header("Movement")]
    public float moveSpeed = 6f;          // 固定移动速度

    
    

    // 跳跃功能的参数
    [Header("Jump")]
    public float jumpForce = 20f;         // 跳跃力度
    public bool allowDoubleJump = false;  // 允许双跳
    public float highJumpMultiplier = 1.5f; // 高跳倍数
    
    [Header("Jump Detection Settings")]
    [SerializeField] private float jumpBufferTime = 0.2f; // 跳跃缓冲时间
    [SerializeField] private float coyoteTime = 0.1f; // 土狼时间(离开地面后仍可跳跃的时间)
    [SerializeField] private float jumpCooldown = 0.1f; // 跳跃冷却时间
    [SerializeField] private float minJumpHeight = 0.5f; // 最小跳跃高度
    [SerializeField] private bool useJumpBuffer = true; // 使用跳跃缓冲
    [SerializeField] private bool useCoyoteTime = true; // 使用土狼时间

    // 下落功能的参数(暂时未用,预留扩展)
    [Header("Fall")]
    public float fallSpeed = -10f;        // 固定下落速度,负值表示向下,暂时没有gravity
    public LayerMask groundLayer;
    public float groundCheckDistance = 0.1f;
    public Vector2 groundCheckBoxSize = new Vector2(0.5f, 0.05f);

    // 检测角色前方是否有墙壁
    [Header("Wall Detection")]
    public float wallCheckDistance = 0.1f; // 前方墙壁检测射线长度
    public Vector2 wallCheckOffset = new Vector2(0.5f, 0f); // 偏移在角色中心点的偏移,水平方向

    // 技能功能预留占位
    [Header("Skills (placeholders)")]
    public KeyCode dashKey = KeyCode.L;   // 位移技能占位
    public KeyCode skill1Key = KeyCode.U;
    public KeyCode skill2Key = KeyCode.I;
    public KeyCode skill3Key = KeyCode.O;

    // 内部状态
    private float inputX;
    private bool wantJump;
    private bool isGrounded;
    private bool doubleJumpUsed;
    private int facing = 1; // 1 右, -1 左,用于表示角色的朝向
    
    // 角色状态
    [Header("Character Stats")]
    public float maxHealth = 100f;
    [SerializeField] private float currentHealth = 100f;
    public float healthRegenPerSec = 1f;
    public float defense = 10f;  // 防御力
    
    // 跳跃状态管理
    private float jumpBufferTimer = 0f; // 跳跃缓冲计时器
    private float coyoteTimer = 0f; // 土狼时间计时器
    private float lastJumpTime = 0f; // 上次跳跃时间
    private bool wasGroundedLastFrame = false; // 上一帧是否在地面
    private float jumpStartY = 0f; // 跳跃开始时的Y位置
    private bool isJumping = false; // 是否正在跳跃

    void Reset()
    {
        rb = GetComponent<Rigidbody2D>();
        bodyCollider = GetComponent<Collider2D>();
    }

    void Awake()
    {
        if (rb == null) rb = GetComponent<Rigidbody2D>();
        if (bodyCollider == null) bodyCollider = GetComponent<Collider2D>();
    }

    void CheckGround()
    {
        if (bodyCollider == null) { isGrounded = false; return; }

        // 使用多射线检测提高精度
        float skin = 0.02f;
        float checkDistance = groundCheckDistance + skin;
        float colliderWidth = bodyCollider.bounds.size.x;
        float colliderBottom = bodyCollider.bounds.min.y;
        
        bool hitGround = false;
        
        // 在角色底部创建多个检测点
        for (int i = 0; i < groundCheckRays; i++)
        {
            float xOffset = (i / (float)(groundCheckRays - 1) - 0.5f) * colliderWidth * 0.8f;
            Vector2 rayOrigin = new Vector2(bodyCollider.bounds.center.x + xOffset, colliderBottom);
            
            RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.down, checkDistance, groundLayer);
            
            if (hit.collider != null)
            {
                hitGround = true;
                Debug.DrawRay(rayOrigin, Vector2.down * checkDistance, Color.green, 0.02f);
            }
            else
            {
                Debug.DrawRay(rayOrigin, Vector2.down * checkDistance, Color.red, 0.02f);
            }
        }
        
        // 检查垂直速度
        bool verticalOk = rb == null ? true : rb.velocity.y <= 0.1f;
        isGrounded = hitGround && verticalOk;
        
        // 更新跳跃状态
        UpdateJumpState();
        
        if (isGrounded) 
        {
            doubleJumpUsed = false;
            isJumping = false;
        }
    }
    
    // 更新跳跃状态
    void UpdateJumpState()
    {
        // 更新土狼时间
        if (useCoyoteTime)
        {
            if (isGrounded)
            {
                coyoteTimer = coyoteTime;
            }
            else
            {
                coyoteTimer -= Time.fixedDeltaTime;
            }
        }
        
        // 更新跳跃缓冲
        if (useJumpBuffer)
        {
            if (jumpBufferTimer > 0)
            {
                jumpBufferTimer -= Time.fixedDeltaTime;
            }
        }
        
        // 更新跳跃移动状态
        UpdateJumpMovementState();
        
        // 记录上一帧的地面状态
        wasGroundedLastFrame = isGrounded;
    }
    
    // 更新跳跃移动状态
    void UpdateJumpMovementState()
    {
        // 检测开始跳跃
        if (isJumping && !wasGroundedLastFrame && isGrounded)
        {
            OnJumpStart();
        }
        
        // 检测结束跳跃(落地)
        if (isJumping && !isGrounded && wasGroundedLastFrame)
        {
            OnJumpEnd();
        }
        
        // 确保在地面时清除跳跃状态
        if (isGrounded && isJumping)
        {
            isJumping = false;
        }
    }
    
    
    // 跳跃结束时的处理
    void OnJumpEnd()
    {
        // 重置跳跃相关状态
        jumpStartY = 0f;
    }
    
    // 检查是否可以跳跃
    bool CanJump()
    {
        // 检查跳跃冷却
        if (Time.time - lastJumpTime < jumpCooldown)
            return false;
            
        // 检查是否在地面或土狼时间内
        bool canGroundJump = isGrounded || (useCoyoteTime && coyoteTimer > 0);
        
        // 检查双跳
        bool canDoubleJump = allowDoubleJump && !doubleJumpUsed && !isGrounded;
        
        return canGroundJump || canDoubleJump;
    }
    
    // 处理跳跃输入缓冲
    void HandleJumpInput()
    {
        if (wantJump)
        {
            if (useJumpBuffer)
            {
                jumpBufferTimer = jumpBufferTime;
            }
            wantJump = false;
        }
    }
    
    // 检查跳跃缓冲
    bool HasJumpBuffer()
    {
        return useJumpBuffer && jumpBufferTimer > 0;
    }

    // 改进的墙壁检测 - 考虑角落情况
    bool IsWallAhead()
    {
        // 使用多射线检测防止边缘穿模
        float colliderHeight = bodyCollider.bounds.size.y;
        float colliderCenterY = bodyCollider.bounds.center.y;
        
        int wallHits = 0; // 计算墙壁命中次数
        
        for (int i = 0; i < wallCheckRays; i++)
        {
            float yOffset = (i / (float)(wallCheckRays - 1) - 0.5f) * colliderHeight * 0.8f;
            Vector2 origin = new Vector2(
                bodyCollider.bounds.center.x + wallCheckOffset.x * facing,
                colliderCenterY + yOffset
            );
            
        RaycastHit2D hit = Physics2D.Raycast(origin, new Vector2(facing, 0f), wallCheckDistance, groundLayer);
            
            if (hit.collider != null)
            {
                wallHits++;
                Debug.DrawRay(origin, new Vector2(facing, 0f) * wallCheckDistance, Color.red, 0.02f);
            }
            else
            {
                Debug.DrawRay(origin, new Vector2(facing, 0f) * wallCheckDistance, Color.green, 0.02f);
            }
        }
        
        // 如果大部分射线都命中墙壁,认为是真正的墙壁
        // 如果只有少数射线命中,可能是角落,允许通过
        return wallHits > wallCheckRays * 0.6f;
    }
    
    // 根据移动方向检测墙壁
    bool IsWallAhead(int direction)
    {
        // 使用多射线检测防止边缘穿模
        float colliderHeight = bodyCollider.bounds.size.y;
        float colliderCenterY = bodyCollider.bounds.center.y;
        float colliderWidth = bodyCollider.bounds.size.x;
        
        int wallHits = 0; // 计算墙壁命中次数
        string directionName = direction > 0 ? "右" : "左";
        
        Debug.Log($"[墙体检测] 开始检测{directionName}侧墙壁 - 角色位置: {transform.position}, 碰撞器边界: min={bodyCollider.bounds.min}, max={bodyCollider.bounds.max}");
        
        for (int i = 0; i < wallCheckRays; i++)
        {
            float yOffset = (i / (float)(wallCheckRays - 1) - 0.5f) * colliderHeight * 0.8f;
            
            // 修正射线起点:从角色碰撞器边缘开始,而不是从中心偏移
            float colliderEdge = direction > 0 ? 
                bodyCollider.bounds.max.x : bodyCollider.bounds.min.x;
            
            Vector2 origin = new Vector2(
                colliderEdge,
                colliderCenterY + yOffset
            );
            
            RaycastHit2D hit = Physics2D.Raycast(origin, new Vector2(direction, 0f), wallCheckDistance, groundLayer);
            
            if (hit.collider != null)
            {
                wallHits++;
                Debug.Log($"[墙体检测] 射线{i+1}命中墙壁 - 起点: {origin}, 距离: {hit.distance:F3}, 碰撞对象: {hit.collider.name}, 位置: {hit.point}");
                Debug.DrawRay(origin, new Vector2(direction, 0f) * wallCheckDistance, Color.red, 0.02f);
            }
            else
            {
                Debug.Log($"[墙体检测] 射线{i+1}未命中 - 起点: {origin}, 检测距离: {wallCheckDistance}");
                Debug.DrawRay(origin, new Vector2(direction, 0f) * wallCheckDistance, Color.green, 0.02f);
            }
        }
        
        // 如果大部分射线都命中墙壁,认为是真正的墙壁
        // 如果只有少数射线命中,可能是角落,允许通过
        bool hasWall = wallHits > wallCheckRays * 0.6f;
        Debug.Log($"[墙体检测] {directionName}侧检测结果 - 命中数: {wallHits}/{wallCheckRays}, 阈值: {wallCheckRays * 0.6f}, 有墙壁: {hasWall}");
        
        return hasWall;
    }
    
    // 预测性碰撞检测
    bool PredictWallCollision()
    {
        if (rb == null || bodyCollider == null) return false;
        
        // 计算下一帧的预测位置
        Vector2 predictedPosition = (Vector2)transform.position + rb.velocity * collisionPredictionTime;
        
        // 检查预测位置是否会碰撞
        float colliderHeight = bodyCollider.bounds.size.y;
        float colliderCenterY = bodyCollider.bounds.center.y;
        
        for (int i = 0; i < wallCheckRays; i++)
        {
            float yOffset = (i / (float)(wallCheckRays - 1) - 0.5f) * colliderHeight * 0.8f;
            Vector2 origin = new Vector2(
                predictedPosition.x + wallCheckOffset.x * facing,
                colliderCenterY + yOffset
            );
            
        RaycastHit2D hit = Physics2D.Raycast(origin, new Vector2(facing, 0f), wallCheckDistance, groundLayer);
            
            if (hit.collider != null)
            {
                return true;
            }
        }
        
        return false;
    }
    
    // 限制速度防止穿模
    Vector2 LimitVelocity(Vector2 velocity)
    {
        // 如果速度超过安全速度,进行限制
        if (Mathf.Abs(velocity.x) > maxSafeSpeed)
        {
            velocity.x = Mathf.Sign(velocity.x) * maxSafeSpeed;
        }
        
        return velocity;
    }


    void HandleMovement()
    {
        // 检测A/D按键输入
        bool pressingA = Input.GetKey(KeyCode.A);
        bool pressingD = Input.GetKey(KeyCode.D);
        
        // 计算移动方向
        float moveDirection = 0f;
        int moveDirectionInt = 0;
        if (pressingA && !pressingD)
        {
            moveDirection = -1f; // 向左
            moveDirectionInt = -1;
        }
        else if (pressingD && !pressingA)
        {
            moveDirection = 1f; // 向右
            moveDirectionInt = 1;
        }
        // 如果同时按下A和D,或者都没按,则停止移动
        
        // 记录输入状态
        if (moveDirectionInt != 0)
        {
            string directionName = moveDirectionInt > 0 ? "右" : "左";
            Debug.Log($"[移动处理] 检测到{directionName}移动输入 - A:{pressingA}, D:{pressingD}, 当前朝向:{facing}");
        }
        
        // 检查墙壁碰撞 - 使用移动方向而不是朝向
        bool wallAhead = false;
        if (moveDirectionInt != 0)
        {
            wallAhead = IsWallAhead(moveDirectionInt);
        }
        
        // 计算目标速度
        float targetVelX = 0f;
        if (moveDirection != 0f && !wallAhead)
        {
            targetVelX = moveDirection * moveSpeed;
            Debug.Log($"[移动处理] 允许移动 - 目标速度: {targetVelX}, 移动速度: {moveSpeed}");
        }
        else if (moveDirection != 0f && wallAhead)
        {
            Debug.Log($"[移动处理] 被墙壁阻挡 - 无法移动");
        }
        else if (moveDirection == 0f)
        {
            Debug.Log($"[移动处理] 无移动输入 - 停止移动");
        }
        
        // 直接设置速度(固定速度移动)
        Vector2 velocity = rb.velocity;
        velocity.x = targetVelX;
        rb.velocity = velocity;
        
        // 更新朝向
        if (moveDirection > 0) facing = 1;
        else if (moveDirection < 0) facing = -1;
        
        // 记录最终状态
        if (moveDirectionInt != 0)
        {
            Debug.Log($"[移动处理] 最终状态 - 目标速度: {targetVelX}, 实际速度: {rb.velocity.x}, 朝向: {facing}, 有墙壁: {wallAhead}");
        }
    }
    
    
    void HandleJump()
    {
        // 处理跳跃输入缓冲
        HandleJumpInput();

        // 检查是否有跳跃缓冲或直接跳跃输入
        bool shouldJump = HasJumpBuffer() || wantJump;
        
        if (shouldJump && CanJump())
        {
            DoJump();
            
            // 清除跳跃缓冲
            jumpBufferTimer = 0f;
            
            // 如果是双跳,标记已使用
            if (!isGrounded && allowDoubleJump && !doubleJumpUsed)
        {
            doubleJumpUsed = true;
            }
            
            // 更新跳跃状态
            lastJumpTime = Time.time;
            isJumping = true;
            jumpStartY = transform.position.y;
        }
        
        // 检查跳跃高度限制
        CheckJumpHeight();
    }
    

    void DoJump()
    {
        // 应用垂直速度,预留高跳倍数,实际实现时根据需求调整
        float appliedJump = jumpForce * highJumpMultiplier;
        
        // 如果是双跳,稍微减少力度
        if (!isGrounded && allowDoubleJump)
        {
            appliedJump *= 0.8f;
        }
        
        Debug.Log($"DoJump appliedJump={appliedJump}, isGrounded={isGrounded}, coyoteTimer={coyoteTimer:F2}");
        
        Vector2 v = rb.velocity;
        v.y = appliedJump;
        rb.velocity = v;
        
        // 清除土狼时间
        if (useCoyoteTime)
        {
            coyoteTimer = 0f;
        }
    }

    /*
    void HandleFallFix()
    {
        // ��������������Ҫ�̶������ٶȣ�ǿ�������ٶȣ������Ҫ��
        if (!isGrounded)
        {
            if (rb.velocity.y < fallSpeed) // fallSpeed �Ǹ�ֵ�����ͱ�ʾ��������
            {
                Vector2 v = rb.velocity;
                v.y = fallSpeed;
                rb.velocity = v;
            }
        }
    }
    */

    // 注意:技能现在由SM_SkillSystem处理
    // 如果需要自定义技能逻辑,可以在这里添加

    // ========== SM_ICharacterProvider 接口实现 ==========
    public Transform AimOrigin => aimOrigin != null ? aimOrigin : transform;
    public Vector2 AimDirection => new Vector2(facing, 0f); // 基于朝向的瞄准方向
    public float CurrentMP => skillSystem != null ? skillSystem.CurrentMP : 0f;
    public float MaxMP => skillSystem != null ? skillSystem.MaxMP : 0f;
    public bool ConsumeMP(float amount) => skillSystem != null ? skillSystem.ConsumeMP(amount) : false;
    
    // ========== SM_IDamageable 接口实现 ==========
    public void ApplyDamage(SM_DamageInfo info)
    {
        float finalDamage = info.Amount;
        
        // 计算防御减免(物理伤害受防御影响)
        if (!info.IgnoreDefense && info.Element == SM_Element.Physical)
        {
            finalDamage = Mathf.Max(1f, finalDamage - defense);
        }
        
        // 计算暴击
        if (Random.value < info.CritChance)
        {
            finalDamage *= info.CritMultiplier;
            Debug.Log($"[伤害] 暴击!造成 {finalDamage} 点伤害");
        }
        
        currentHealth = Mathf.Max(0f, currentHealth - finalDamage);
        Debug.Log($"[伤害] 受到 {finalDamage} 点 {info.Element} 伤害,剩余生命值: {currentHealth}/{maxHealth}");
        
        if (currentHealth <= 0f)
        {
            OnDeath();
        }
    }
    
    public Transform GetTransform() => transform;
    
    // ========== 角色状态管理 ==========
    private void OnDeath()
    {
        Debug.Log("[角色] 角色死亡!");
        // 这里可以添加死亡逻辑,比如播放动画、禁用控制等
        enabled = false;
    }
    
    private void UpdateHealth()
    {
        // 生命值回复
        if (currentHealth < maxHealth)
        {
            currentHealth = Mathf.Min(maxHealth, currentHealth + healthRegenPerSec * Time.deltaTime);
        }
    }
    
    // ========== 技能系统集成 ==========
    private void UpdateSkillAim()
    {
        // 更新技能系统的瞄准方向
        if (skillSystem != null)
        {
            skillSystem.SetAim(new Vector2(facing, 0f));
        }
    }

    void Start()
    {
        // 确保组件存在
        if (rb == null) rb = GetComponent<Rigidbody2D>();
        if (bodyCollider == null) bodyCollider = GetComponent<Collider2D>();
        if (skillSystem == null) skillSystem = GetComponent<SM_SkillSystem>();
        
        // 初始化生命值
        currentHealth = maxHealth;

        if (rb == null)
        {
            Debug.LogError("[PlayerController] Rigidbody2D 未找到,禁用脚本", this);
            enabled = false;
            return;
        }
        if (bodyCollider == null)
        {
            Debug.LogError("[PlayerController] Collider2D 未找到,禁用脚本", this);
            enabled = false;
            return;
        }

        // 配置连续碰撞检测防止高速穿模
        if (useContinuousCollisionDetection)
        {
            rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
        }
        else
        {
            rb.collisionDetectionMode = CollisionDetectionMode2D.Discrete;
        }

        // 验证参数值,防止异常行为
        moveSpeed = Mathf.Max(0.01f, moveSpeed);

        // 验证碰撞检测参数
        groundCheckRays = Mathf.Max(1, groundCheckRays);
        wallCheckRays = Mathf.Max(1, wallCheckRays);
        collisionPredictionTime = Mathf.Max(0.01f, collisionPredictionTime);
        maxSafeSpeed = Mathf.Max(moveSpeed, maxSafeSpeed);
        
        // 验证跳跃参数
        jumpBufferTime = Mathf.Max(0f, jumpBufferTime);
        coyoteTime = Mathf.Max(0f, coyoteTime);
        jumpCooldown = Mathf.Max(0f, jumpCooldown);
        minJumpHeight = Mathf.Max(0f, minJumpHeight);
        
        Debug.Log($"[PlayerController] 初始化完成 - CCD: {useContinuousCollisionDetection}, 地面射线: {groundCheckRays}, 墙壁射线: {wallCheckRays}, 移动速度: {moveSpeed}");
        Debug.Log($"[PlayerController] 跳跃设置 - 缓冲时间: {jumpBufferTime}, 土狼时间: {coyoteTime}, 冷却: {jumpCooldown}, 最小高度: {minJumpHeight}");
    }

    // Update is called once per frame
    void Update()
    {
        // 检测跳跃输入
        if (Input.GetKeyDown(KeyCode.K))
        {
            wantJump = true;
        }

        // 更新技能瞄准方向
        UpdateSkillAim();
        
        // 更新生命值
        UpdateHealth();
        
        // 注意:技能按键检测现在由SM_SkillSystem自动处理
        // 如果需要自定义技能逻辑,可以在这里添加
    }

    void FixedUpdate()
    {
        CheckGround();
        
        // 更新跳跃状态
        UpdateJumpMovementState();
        
        // 处理移动
        HandleMovement();
        
        // 处理跳跃
        HandleJump();
        
        // 更新状态
        UpdateStates();
        
        // 更新计时器
        UpdateTimers();
        
        // 重置跳跃输入
        wantJump = false;
    }

    // 绘制检测范围(调试用)
    void OnDrawGizmosSelected()
    {
        if (bodyCollider == null) return;

        // 绘制地面检测射线
        float skin = 0.02f;
        float checkDistance = groundCheckDistance + skin;
        float colliderWidth = bodyCollider.bounds.size.x;
        float colliderBottom = bodyCollider.bounds.min.y;
        
        Gizmos.color = Color.cyan;
        for (int i = 0; i < groundCheckRays; i++)
        {
            float xOffset = (i / (float)(groundCheckRays - 1) - 0.5f) * colliderWidth * 0.8f;
            Vector3 rayOrigin = new Vector3(bodyCollider.bounds.center.x + xOffset, colliderBottom, transform.position.z);
            Vector3 rayEnd = rayOrigin + Vector3.down * checkDistance;
            
            Gizmos.DrawLine(rayOrigin, rayEnd);
        }
        
        // 绘制墙壁检测射线
        float colliderHeight = bodyCollider.bounds.size.y;
        float colliderCenterY = bodyCollider.bounds.center.y;
        
        Gizmos.color = Color.magenta;
        for (int i = 0; i < wallCheckRays; i++)
        {
            float yOffset = (i / (float)(wallCheckRays - 1) - 0.5f) * colliderHeight * 0.8f;
            Vector3 rayOrigin = new Vector3(
                bodyCollider.bounds.center.x + wallCheckOffset.x * facing,
                colliderCenterY + yOffset,
                transform.position.z
            );
            Vector3 rayEnd = rayOrigin + Vector3.right * facing * wallCheckDistance;
            
            Gizmos.DrawLine(rayOrigin, rayEnd);
        }
        
        // 绘制预测位置
        if (rb != null)
        {
            Vector3 predictedPos = transform.position + (Vector3)(rb.velocity * collisionPredictionTime);
        Gizmos.color = Color.yellow;
            Gizmos.DrawWireSphere(predictedPos, 0.1f);
        }
        
        // 绘制速度限制范围
        if (rb != null && Mathf.Abs(rb.velocity.x) > maxSafeSpeed)
        {
            Gizmos.color = Color.red;
            Vector3 warningPos = transform.position + Vector3.up * 2f;
            Gizmos.DrawWireSphere(warningPos, 0.2f);
        }
        
        // 绘制跳跃状态
        if (isJumping)
        {
            Gizmos.color = Color.cyan;
            Vector3 jumpPos = transform.position + Vector3.up * 1.5f;
            Gizmos.DrawWireSphere(jumpPos, 0.15f);
        }
        
        // 绘制土狼时间状态
        if (useCoyoteTime && coyoteTimer > 0)
        {
        Gizmos.color = Color.yellow;
            Vector3 coyotePos = transform.position + Vector3.up * 1f;
            Gizmos.DrawWireCube(coyotePos, Vector3.one * 0.1f);
        }
        
        // 绘制跳跃缓冲状态
        if (useJumpBuffer && jumpBufferTimer > 0)
        {
            Gizmos.color = Color.green;
            Vector3 bufferPos = transform.position + Vector3.up * 0.5f;
            Gizmos.DrawWireCube(bufferPos, Vector3.one * 0.08f);
        }
        
        // 绘制移动状态(简化版本)
        if (isGrounded)
        {
            Gizmos.color = Color.green;
            Vector3 groundPos = transform.position + Vector3.up * 2.5f;
            Gizmos.DrawWireCube(groundPos, Vector3.one * 0.1f);
        }
    }
    
    
    // 更新状态
    void UpdateStates()
    {
        // 朝向在HandleMovement中已经更新,这里不需要额外处理
    }
    
    // 更新计时器
    void UpdateTimers()
    {
        // 更新跳跃缓冲计时器
        if (jumpBufferTimer > 0)
        {
            jumpBufferTimer -= Time.fixedDeltaTime;
        }
        
        // 更新土狼时间计时器
        if (coyoteTimer > 0)
        {
            coyoteTimer -= Time.fixedDeltaTime;
        }
        
        // 无输入计时器已移除,简化移动逻辑不需要
    }
    
    // 跳跃开始事件
    void OnJumpStart()
    {
        isJumping = true;
        jumpStartY = transform.position.y;
    }
    
    // 落地事件
    void OnLanding()
    {
        // 重置双跳状态
        doubleJumpUsed = false;
    }
    
    // 起飞事件
    void OnTakeOff()
    {
        // 起飞时不需要特殊处理
    }
    
    // 检查跳跃高度
    void CheckJumpHeight()
    {
        if (isJumping)
        {
            float currentHeight = transform.position.y - jumpStartY;
            
            // 如果达到最小跳跃高度且正在下降,结束跳跃状态
            if (currentHeight >= minJumpHeight && rb.velocity.y <= 0)
            {
                OnJumpEnd();
            }
        }
    }
}


img

攻击模块:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Attack : MonoBehaviour
{
    [Header("攻击属性")]
    [SerializeField] private AttackType attackType = AttackType.Physical; // 攻击类型,默认物理攻击
    [SerializeField] private KeyCode attackKey = KeyCode.J; // 攻击按键,默认为J键
    [SerializeField] private float attackRange = 100f; // 攻击范围
    [SerializeField] private float attackCooldown = 0.5f; // 攻击冷却时间
    [SerializeField] private LayerMask enemyLayer; // 敌人层级

    [Header("攻击判定")]
    [SerializeField] private Transform attackPoint; // 攻击判定点
    [SerializeField] private float attackRadius = 100f; // 攻击判定半径
    [SerializeField] private bool useBoxDetection = false; // 是否使用Box检测
    [SerializeField] private Vector2 boxHalfExtents = new Vector2(1f, 1f); // Box检测大小的一半
    [SerializeField] private Vector2 attackOffset = Vector2.zero; // 攻击点偏移量

    [Header("物理攻击属性")]
    [SerializeField] private float physicalCritRate = 0.1f; // 物理暴击率
    [SerializeField] private float physicalCritDamage = 1.5f; // 物理暴击伤害倍数

    [Header("元素攻击属性")]
    [SerializeField] private float elementalEffectChance = 0.7f; // 元素效果触发概率
    [SerializeField] private float fireDamageDuration = 3f; // 火元素持续伤害时间
    [SerializeField] private float iceFreezeDuration = 2f; // 冰元素冻结时间
    [SerializeField] private float windKnockbackForce = 5f; // 风元素击退力度
    [SerializeField] private float thunderChainRange = 3f; // 雷元素连锁范围

    // 攻击类型枚举
    public enum AttackType
    {
        Physical, // 物理攻击:无视防御,低暴击
        Fire,     // 火元素:持续伤害
        Wind,     // 风元素:击退效果
        Ice,      // 冰元素:冻结控制
        Thunder   // 雷元素:范围连锁
    }

    private float lastAttackTime; // 上次攻击时间
    private Attribute attackerAttribute; // 攻击者属性
    private int facingDirection = 1; // 人物朝向,1为右,-1为左

    // 属性访问器
    public float AttackRange => attackRange;
    public bool CanAttack => Time.time >= lastAttackTime + attackCooldown;
    public int CurrentAttack => attackerAttribute != null ? attackerAttribute.Attack : 0;
    public AttackType Type => attackType;

    // 事件
    public System.Action<GameObject, int, AttackType> OnAttackHit; // 攻击命中事件
    public System.Action<AttackType> OnAttackPerformed; // 攻击执行事件
    public System.Action<AttackType, GameObject> OnElementEffectApplied; // 元素效果应用事件

    // 攻击信息结构体
    public struct AttackInfo
    {
        public GameObject target;
        public int baseDamage;
        public AttackType type;
        public bool isCrit;
        public Vector3 hitPoint;
    }

    private void Awake()
    {
        attackerAttribute = GetComponent<Attribute>();

        // 获取初始朝向
        UpdateFacingDirection();
    }

    private void Update()
    {
        // 更新人物朝向
        UpdateFacingDirection();

        if (Input.GetKeyDown(KeyCode.J))
        {
            PerformAttack();
        }

        if (Input.GetKeyDown(KeyCode.Mouse0))
        {
            Debug.Log("鼠标左键被按下");
        }

        if (Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log("空格键被按下");
        }
    }

    // 更新人物朝向
    private void UpdateFacingDirection()
    {
        // 根据localScale.x判断朝向,或者根据速度方向判断
        if (transform.localScale.x != 0)
        {
            facingDirection = transform.localScale.x > 0 ? 1 : -1;
        }
    }

    // 获取实际攻击点位置(考虑偏移和朝向)
    private Vector2 GetActualAttackPosition()
    {
        Vector2 basePosition = attackPoint != null ? (Vector2)attackPoint.position : (Vector2)transform.position;

        // ✅ 获取 PlayerController 的朝向(如果存在)
        int facing = facingDirection;
        PlayerController controller = GetComponent<PlayerController>();
        if (controller != null)
        {
            // 使用 PlayerController 的 facing 值(1 表右,-1 表左)
            System.Reflection.FieldInfo facingField = typeof(PlayerController).GetField("facing", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            if (facingField != null)
            {
                facing = (int)facingField.GetValue(controller);
            }
        }

        // ✅ 根据朝向镜像 X 偏移
        Vector2 mirroredOffset = new Vector2(
            Mathf.Abs(attackOffset.x) * facing,
            attackOffset.y
        );

        return basePosition + mirroredOffset;
    }

    public void PerformAttack()
    {
        if (!CanAttack)
        {
            Debug.Log($"攻击冷却中,剩余时间:{lastAttackTime + attackCooldown - Time.time}");
            return;
        }

        Debug.Log("执行攻击");
        OnAttackPerformed?.Invoke(attackType);

        Collider2D[] hitEnemies = GetHitEnemies();
        Debug.Log($"检测到{hitEnemies.Length}个敌人");

        foreach (Collider2D enemy in hitEnemies)
        {
            if (enemy.gameObject != gameObject)
            {
                ProcessAttackHit(enemy.gameObject);
            }
        }

        lastAttackTime = Time.time;
    }

    public void AttackTarget(GameObject target)
    {
        if (target == null || !CanAttack) return;

        OnAttackPerformed?.Invoke(attackType);
        ProcessAttackHit(target);
        lastAttackTime = Time.time;
    }

    // ✅ 修改部分:随朝向镜像攻击检测范围
    private Collider2D[] GetHitEnemies()
    {
        Vector2 actualAttackPosition = GetActualAttackPosition();

        if (useBoxDetection)
        {
            // 根据朝向决定盒子的旋转方向
            float rotationZ = attackPoint != null ? attackPoint.eulerAngles.z : 0f;
            if (facingDirection < 0)
            {
                rotationZ = 180f - rotationZ; // 镜像旋转
            }

            return Physics2D.OverlapBoxAll(
                actualAttackPosition,
                boxHalfExtents * 2, // 转换为完整尺寸
                rotationZ,
                enemyLayer
            );
        }
        else
        {
            // 使用2D圆形检测
            return Physics2D.OverlapCircleAll(
                actualAttackPosition,
                attackRadius,
                enemyLayer
            );
        }
    }

    [ContextMenu("强制设置测试层级")]
    private void ForceTestLayers()
    {
        enemyLayer = LayerMask.GetMask("Everything");
        Debug.Log("已设置为检测所有层级,重新测试攻击");
    }

    [ContextMenu("测试物理系统")]
    private void TestPhysicsSystem()
    {
        Vector2 actualAttackPosition = GetActualAttackPosition();
        Collider2D[] hits = Physics2D.OverlapCircleAll(actualAttackPosition, 5f);
        Debug.Log($"5米范围内所有碰撞体: {hits.Length}");

        if (hits.Length == 0)
        {
            Debug.LogError("物理检测完全无结果!检查:");
            Debug.LogError("1. 碰撞体是否存在且未设置为Trigger");
            Debug.LogError("2. 对象位置是否正确");
            Debug.LogError("3. 物理系统是否正常工作");
        }
    }

    [ContextMenu("测试层级配置")]
    private void TestLayerConfiguration()
    {
        Debug.Log("=== 层级配置测试 ===");

        if (enemyLayer.value == 0)
        {
            Debug.LogError("enemyLayer为0(Nothing)!这是问题所在!");
            return;
        }

        string[] layerNames = GetLayerNamesFromMask(enemyLayer);
        Debug.Log($"当前检测的层级: {string.Join(", ", layerNames)}");
    }

    private string[] GetLayerNamesFromMask(LayerMask mask)
    {
        List<string> layers = new List<string>();
        for (int i = 0; i < 32; i++)
        {
            if ((mask.value & (1 << i)) != 0)
            {
                layers.Add(LayerMask.LayerToName(i));
            }
        }
        return layers.ToArray();
    }

    private void ProcessAttackHit(GameObject target)
    {
        if (target == null) return;

        int baseDamage = attackerAttribute != null ? attackerAttribute.Attack : 0;
        if (baseDamage <= 0) return;

        AttackInfo attackInfo = new AttackInfo
        {
            target = target,
            baseDamage = baseDamage,
            type = attackType,
            hitPoint = target.transform.position
        };

        int finalDamage = CalculateDamage(attackInfo);
        OnAttackHit?.Invoke(target, finalDamage, attackType);
        ApplyDamage(target, finalDamage, attackInfo);

        if (attackType != AttackType.Physical)
        {
            ApplyElementEffect(target, attackInfo);
        }
    }

    private int CalculateDamage(AttackInfo attackInfo)
    {
        int baseDamage = attackInfo.baseDamage;

        switch (attackType)
        {
            case AttackType.Physical:
                bool isCrit = Random.value < physicalCritRate;
                attackInfo.isCrit = isCrit;
                int physicalDamage = Mathf.RoundToInt(baseDamage * (isCrit ? physicalCritDamage : 1f));
                Debug.Log($"物理攻击: 基础{baseDamage}, 暴击{isCrit}, 最终{physicalDamage}");
                return physicalDamage;

            default:
                Attribute targetAttribute = attackInfo.target.GetComponent<Attribute>();
                if (targetAttribute != null)
                {
                    float defenseMultiplier = Mathf.Clamp(1f - (targetAttribute.Defense * 0.01f), 0.1f, 1f);
                    int elementalDamage = Mathf.RoundToInt(baseDamage * defenseMultiplier);
                    Debug.Log($"元素攻击: 基础{baseDamage}, 防御减伤{defenseMultiplier:P0}, 最终{elementalDamage}");
                    return elementalDamage;
                }
                return baseDamage;
        }
    }

    private void ApplyDamage(GameObject target, int damage, AttackInfo attackInfo)
    {
        Attribute targetAttribute = target.GetComponent<Attribute>();
        if (targetAttribute != null)
        {
            if (attackType == AttackType.Physical)
            {
                targetAttribute.TakeTrueDamage(damage, gameObject);
                Debug.Log($"物理攻击无视防御,造成 {damage} 点真实伤害");
            }
            else
            {
                targetAttribute.TakeDamage(damage, gameObject);
                Debug.Log($"元素攻击经过防御减伤,造成 {damage} 点伤害");
            }
        }
    }

    private void ApplyElementEffect(GameObject target, AttackInfo attackInfo)
    {
        if (Random.value > elementalEffectChance) return;

        switch (attackType)
        {
            case AttackType.Fire:
                ApplyFireEffect(target, attackInfo);
                break;
            case AttackType.Wind:
                ApplyWindEffect(target, attackInfo);
                break;
            case AttackType.Ice:
                ApplyIceEffect(target, attackInfo);
                break;
            case AttackType.Thunder:
                ApplyThunderEffect(target, attackInfo);
                break;
        }

        OnElementEffectApplied?.Invoke(attackType, target);
    }

    private void ApplyFireEffect(GameObject target, AttackInfo attackInfo)
    {
        BurnEffect burnEffect = target.GetComponent<BurnEffect>();
        if (burnEffect == null) burnEffect = target.AddComponent<BurnEffect>();

        int burnDamage = Mathf.RoundToInt(attackInfo.baseDamage * 0.3f);
        burnEffect.StartBurning(burnDamage, fireDamageDuration, gameObject);
        Debug.Log($"火元素: {target.name} 开始燃烧,持续{fireDamageDuration}秒");
    }

    // ✅ 修改:风元素击退方向改为角色面朝方向
    private void ApplyWindEffect(GameObject target, AttackInfo attackInfo)
    {
        if (target == null) return;

        EnemyAI enemyAI = target.GetComponent<EnemyAI>();
        if (enemyAI != null)
        {
            // 使用角色面朝方向决定击退方向
            bool fromRight = facingDirection < 0; // 如果角色朝左,则击退力来自右边

            // 直接调用 EnemyAI 的击退方法,使用 EnemyAI 中定义的击退力度
            enemyAI.ApplyWindKnockback(enemyAI.windKnockbackForce, fromRight);

            Debug.Log($"💨 风元素: 对 {target.name} 触发击退 (force={enemyAI.windKnockbackForce}, fromRight={fromRight}, facingDirection={facingDirection})");
            return;
        }

        // 备用方案:如果目标没有 EnemyAI 组件
        Rigidbody2D targetRb = target.GetComponent<Rigidbody2D>();
        if (targetRb != null)
        {
            // 根据角色面朝方向决定击退方向
            float horizontal = facingDirection * windKnockbackForce;
            targetRb.velocity = new Vector2(horizontal, targetRb.velocity.y);

            Debug.Log($"风元素 -> 对 {target.name} 直接设置水平速度 (vx={horizontal}, facingDirection={facingDirection})");
        }
    }

    private void ApplyIceEffect(GameObject target, AttackInfo attackInfo)
    {
        FreezeEffect freezeEffect = target.GetComponent<FreezeEffect>();
        if (freezeEffect == null) freezeEffect = target.AddComponent<FreezeEffect>();

        freezeEffect.StartFreeze(iceFreezeDuration);
        Debug.Log($"冰元素: {target.name} 被冻结{iceFreezeDuration}秒");
    }

    private void ApplyThunderEffect(GameObject target, AttackInfo attackInfo)
    {
        Collider2D[] nearbyEnemies = Physics2D.OverlapCircleAll(target.transform.position, thunderChainRange, enemyLayer);
        int chainDamage = Mathf.RoundToInt(attackInfo.baseDamage * 0.5f);

        foreach (Collider2D enemy in nearbyEnemies)
        {
            if (enemy.gameObject != target && enemy.gameObject != gameObject)
            {
                Attribute enemyAttribute = enemy.GetComponent<Attribute>();
                if (enemyAttribute != null)
                {
                    enemyAttribute.TakeDamage(chainDamage, gameObject);
                    Debug.Log($"雷元素连锁: {enemy.name} 受到{chainDamage}伤害");
                }
            }
        }

        Debug.Log($"雷元素: 连锁攻击{nearbyEnemies.Length - 1}个目标");
    }

    public void SetAttackType(AttackType newType) => attackType = newType;
    public void SetAttackRange(float newRange) => attackRange = Mathf.Max(0f, newRange);
    public void SetAttackCooldown(float newCooldown) => attackCooldown = Mathf.Max(0f, newCooldown);
    public void SetAttackKey(KeyCode newKey) => attackKey = newKey;

    public void ForceAttack()
    {
        lastAttackTime = Time.time - attackCooldown;
        PerformAttack();
    }

    // ✅ 修改部分:Gizmos绘制随朝向镜像
    private void OnDrawGizmosSelected()
    {
        Gizmos.color = attackType == AttackType.Physical ? Color.red : GetElementColor(attackType);
        Vector2 actualAttackPosition = GetActualAttackPosition();

        if (useBoxDetection)
        {
            float rotationZ = attackPoint != null ? attackPoint.eulerAngles.z : 0f;
            if (Application.isPlaying && facingDirection < 0)
            {
                rotationZ = 180f - rotationZ;
            }

            DrawGizmosBox(actualAttackPosition, boxHalfExtents * 2, rotationZ);
        }
        else
        {
            DrawGizmosCircle(actualAttackPosition, attackRadius);
        }

        Gizmos.color = Color.white;
        Gizmos.DrawWireSphere(actualAttackPosition, 0.1f);

        if (attackType == AttackType.Thunder)
        {
            Gizmos.color = Color.yellow;
            DrawGizmosCircle(transform.position, thunderChainRange);
        }
    }

    private void DrawGizmosBox(Vector3 center, Vector2 size, float angle)
    {
        Vector3[] corners = new Vector3[4];
        float halfWidth = size.x * 0.5f;
        float halfHeight = size.y * 0.5f;

        Quaternion rotation = Quaternion.Euler(0, 0, angle);

        corners[0] = center + rotation * new Vector3(-halfWidth, -halfHeight, 0);
        corners[1] = center + rotation * new Vector3(halfWidth, -halfHeight, 0);
        corners[2] = center + rotation * new Vector3(halfWidth, halfHeight, 0);
        corners[3] = center + rotation * new Vector3(-halfWidth, halfHeight, 0);

        Gizmos.DrawLine(corners[0], corners[1]);
        Gizmos.DrawLine(corners[1], corners[2]);
        Gizmos.DrawLine(corners[2], corners[3]);
        Gizmos.DrawLine(corners[3], corners[0]);
    }

    private void DrawGizmosCircle(Vector3 center, float radius)
    {
        int segments = 32;
        float angle = 0f;

        Vector3 lastPoint = center + new Vector3(Mathf.Cos(0) * radius, Mathf.Sin(0) * radius, 0);

        for (int i = 1; i <= segments; i++)
        {
            angle = i * (2 * Mathf.PI / segments);
            Vector3 nextPoint = center + new Vector3(Mathf.Cos(angle) * radius, Mathf.Sin(angle) * radius, 0);
            Gizmos.DrawLine(lastPoint, nextPoint);
            lastPoint = nextPoint;
        }
    }

    private Color GetElementColor(AttackType type)
    {
        switch (type)
        {
            case AttackType.Fire: return Color.red;
            case AttackType.Wind: return Color.green;
            case AttackType.Ice: return Color.blue;
            case AttackType.Thunder: return Color.yellow;
            default: return Color.white;
        }
    }
}

// 修改效果类以使用2D物理
public class BurnEffect : MonoBehaviour
{
    private float burnTimer;
    private int burnDamage;
    private float interval = 1f;
    private GameObject damageSource;

    public void StartBurning(int damage, float duration, GameObject source)
    {
        burnDamage = damage;
        burnTimer = duration;
        damageSource = source;
        StartCoroutine(BurnCoroutine());
    }

    private IEnumerator BurnCoroutine()
    {
        while (burnTimer > 0)
        {
            yield return new WaitForSeconds(interval);

            Attribute attribute = GetComponent<Attribute>();
            if (attribute != null && attribute.IsAlive)
            {
                attribute.TakeDamage(burnDamage, damageSource);
                Debug.Log($"燃烧伤害: {burnDamage}");
            }

            burnTimer -= interval;
        }

        Destroy(this);
    }
}

public class FreezeEffect : MonoBehaviour
{
    private float freezeTimer;
    private Vector2 originalVelocity;
    private Rigidbody2D rb;

    public void StartFreeze(float duration)
    {
        freezeTimer = duration;
        rb = GetComponent<Rigidbody2D>();

        if (rb != null)
        {
            originalVelocity = rb.velocity;
            rb.velocity = Vector2.zero;
            rb.isKinematic = true;
        }

        StartCoroutine(FreezeCoroutine());
    }

    private IEnumerator FreezeCoroutine()
    {
        Debug.Log($"冻结开始,持续{freezeTimer}秒");
        while (freezeTimer > 0)
        {
            freezeTimer -= Time.deltaTime;
            yield return null;
        }

        if (rb != null)
        {
            rb.isKinematic = false;
            rb.velocity = originalVelocity;
        }

        Debug.Log("冻结结束");
        Destroy(this);
    }
}

技能模块:

using UnityEngine; // Unity 命名空间

/// <summary>
/// 火球术:发射一颗火球,命中后造成伤害并附带燃烧(持续伤害)
/// 需要在 Inspector 里设置 fireballPrefab(一个带 SM_Projectile 的预制体)
/// </summary>
public class SM_Fire_Fireball : SM_BaseSkill
{
    [Header("火球参数")]
    public SM_Projectile fireballPrefab; // 预制体:Sprite + Collider2D(isTrigger) + 本脚本
    public float damage = 25f;           // 直接伤害
    public float burnDPS = 5f;           // 燃烧每秒伤害
    public float burnTime = 4f;          // 燃烧持续时间
    public float speed = 12f;            // 飞行速度
    public float lifetime = 3f;          // 存活时间

    protected override bool DoCast()
    {
        if (fireballPrefab == null) return false;                                   // 未配置预制体
        var go = Instantiate(fireballPrefab, character.AimOrigin.position, Quaternion.identity); // 生成
        go.damage = damage;                                                         // 赋值参数
        go.element = SM_Element.Fire;                                               // 元素:火
        go.burnDPS = burnDPS;                                                       // 燃烧每秒伤害
        go.burnTime = burnTime;                                                     // 燃烧持续
        go.speed = speed;                                                           // 速度
        go.lifetime = lifetime;                                                     // 生命周期
        go.Launch(character.AimDirection);                                          // 发射
        return true;                                                                // 成功
    }
}

...全文
80 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
书名:JSP 2.0 技术手册(电子书) 格式:PDF 出版:电子工业出版社 作者:林上杰、林康司 本书图文并茂,以丰富的实例为引导,全面介绍了主流的 Java Web 开发技术—— JSP 2.0,重点介绍Java在展示层的两项重要技术:Java Servlet与JavaServer Pages。 它们是最重要的 Java 核心技术。对这两项技术的深入了解,将有助于您未来对于 JavaServer Faces(JSF)技术以及Java Web Services技术的学习 目录 第一章 安装执行环境 1-1 安装 J2SDK 1.4.2 1-2 安装 Tomcat 5.0.16 1-3 安装 JSPBook 站台范例 1-4 安装 Ant 1.6 第二章 Servlet 2.4 简介 2-1 Servlet 简介 2-2 First Servlet Sample Code 2-3 Servlet 的生命周期 2-4 Servlet 范例程序 2-5 Servlet 2.4 的新功能 第三章 JSP 2.0 简介 3-1 JavaServer Pages 技术 3-2 What is JSP 3-3 JSP 与 Servlet 的比较 3-4 JSP 的执行过程 3-5 JSP 与 ASP 和 ASP+ 的比较 3-6 JSP 2.0 新功能 第四章 JSP 语法 4-1 Elements 和 Template Data 4-2 批注(Comments) 4-3 Quoting 和 Escape 规则 4-4 Directives Elements 4-5 Scripting Elements 4-6 Action Elements 4-7 错误处理 第五章 隐含对象(Implicit Object) 5-1 属性( Attribute ) 与范围( Scope ) 5-2 与 Servlet 有关的隐含对象 5-3 与 Input / Output 有关的隐含对象 5-4 与 Context 有关的隐含对象 5-5 与 Error 有关的隐含对象 第六章 Expression Language 6-1 EL 简介 6-2 EL 语法 6-3 EL 隐含对象 6-4 EL 算术运算符 6-5 EL 关系运算符 6-6 EL 逻辑运算符 6-7 EL 其他运算符 6-8 EL Functions 第七章 JSTL 1.1 7-1 JSTL 1.1 简介 7-2 核心标签库(Core tag library) 7-3 I18N 格式标签库(I18N-capable formatting tags library) 7-4 SQL 标签库(SQL tag library) 7-5 XML 标签库(XML tag library) 7-6 函数标签库(Functions tag library) 第八章 JSP 与 JavaBean 8-1 JavaBean 的简介 8-2 JSP 与 JavaBean 8-3 JavaBean 的范围 8-4 JavaBean 的移除 第九章 网页窗体的处理 9-1 HTML 窗体如何传送数据 9-2 窗体中常见的输入类型 9-3 JSP 处理窗体 9-4 文件上传—— Oreilly 上传组件 9-5 jspSmartUpload ——上传和下载 9-6 本文区输入类型(Textarea) 第十章 Session Tracking 10-1 Stateful & Stateless 10-2 Session Tracking 的四种方法 10-3 Session 的生命周期 10-4 HttpSessionBindingListener 接口 10-5 Shopping Cart 范例程序一 10-6 Shopping Cart 范例程序二 第十一章 Filter 与 Listener 11-1 Filter 简介 11-2 Filter 的运作方式 11-3 实现阶段第一个 Filter 11-4 对请求做统一的认证处理 11-5 ServletRequest 和 ServletResponse 之 Wrapper 类 11-6 使用 Filter 来解决中文问题 11-7 Listener 接口简介 11-8 ServletContext Listener 11-9 HttpSession Listener 11-10 ServletRequest Listener 第十二章 JSP 执行环境与开发工具 12-1 Tomcat 5.0 的介绍 12-2 JSP 开发工具介绍 12-3 Eclipse 简介与安装 12-4 使用 Eclipse 开发 Hello

103

社区成员

发帖
与我相关
我的任务
社区描述
2501_CS_SE_FZU
软件工程 高校
社区管理员
  • FZU_SE_LQF
  • 木村修
  • 心态773
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧