103
社区成员
发帖
与我相关
我的任务
分享| 这个作业属于哪个课程 | 2501_CS_SE_FZU |
|---|---|
| 这个作业要求在哪里 | 团队作业——站立式会议+α冲刺 |
| 这个作业的目标 | α冲刺Day2 |
| 其他参考文献 | 《构建之法》、Google Style Guides |


| 成员角色 | 昨日至今日日站立会安排 | 存在的问题 / 遇到的困难 | 心得体会 | 今日至明日站立会安排 |
|---|---|---|---|---|
| 陈志豪(敌人模块) | 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)。 |
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
}
}

敌人模块:
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;
}
}
}

地图模块:

角色模块:
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();
}
}
}
}

攻击模块:
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; // 成功
}
}