# 【制作100个unity游戏之26】unity2d横版卷轴动作类游戏6（附带项目源码）

## 敌人

### 撞墙判断

public class PhysicsCheck : MonoBehaviour
{
public Vector2 bottomOffset;// 检测圆形底部偏移量
public bool isGround; // 是否在地面上

private Vector2 leftOffset;
private Vector2 rightOffset;
public bool touchLeftWall;//是否接触到左墙壁
public bool touchRightWall;//是否接触到右墙壁
private CapsuleCollider2D coll;
public bool manual; //是否手动配置

private void Awake() {
coll = GetComponent<CapsuleCollider2D>();
//如果不是手动配置偏移量，则根据 Collider 的位置和大小计算左右偏移量
if(!manual){
Vector2 collPos = coll.offset * (Vector2)transform.localScale;
rightOffset = new Vector2(collPos.x + coll.size.x / 2 + checkRadius, coll.size.y / 2);
leftOffset = new Vector2(collPos.x - coll.size.x / 2 - checkRadius, coll.size.y / 2);
}
}

private void Update()
{
//根据物体的 x 轴缩放来更新偏移量
Check();
}

private void FixedUpdate() {
UpdateOffset(transform.localScale.x);
}

private void UpdateOffset(float facedir){
// 根据物体的 x 轴缩放更新左右偏移量
if(!manual){
Vector2 collPos = coll.offset * facedir;
rightOffset = new Vector2(collPos.x + coll.size.x / 2 + checkRadius, coll.size.y / 2);
leftOffset = new Vector2(collPos.x - coll.size.x / 2 - checkRadius, coll.size.y / 2);
}else{
leftOffset = leftOffset * facedir;
rightOffset = rightOffset * facedir;
}
}

public void Check()
{
// 检测是否在地面上
isGround = Physics2D.OverlapCircle(transform.position + bottomOffset, checkRadius, groundLayer);

//墙壁判断
touchLeftWall = Physics2D.OverlapCircle((Vector2)transform.position + leftOffset, checkRadius, groundLayer);
touchRightWall = Physics2D.OverlapCircle((Vector2)transform.position + rightOffset, checkRadius, groundLayer);
}

private void OnDrawGizmosSelected()
{
// 在 Scene 视图中绘制检测范围
}
}


### 敌人基本AI逻辑实现

public class Enemy : MonoBehaviour
{
Rigidbody2D rb;
protected Animator anim;
private PhysicsCheck physicsCheck;

public float normalSpeed; // 常规速度
public float chaseSpeed; // 追逐速度
public float currentSpeed; // 当前速度
public Vector3 faceDir; // 面向方向
public bool wait;//是否等待
public float waitTime;//等待时长

private void Awake()
{
rb = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
physicsCheck = GetComponent<PhysicsCheck>();
currentSpeed = normalSpeed;
}

private void Update()
{
//面向方向 默认右边为正方向
faceDir = new Vector3(-transform.localScale.x, 0, 0);

//按敌人面向和撞墙 切换敌人状态
if((physicsCheck.touchLeftWall && faceDir.x < 0  || physicsCheck.touchRightWall && faceDir.x > 0) && !wait){
wait = true; // 设为等待状态
anim.SetBool("walk", false);//禁止走路动画
StartCoroutine(WaitTimer()); // 启动等待计时
}
}

private void FixedUpdate()
{
if(!wait) Move();
}

//移动方法
public virtual void Move()
{
anim.SetBool("walk", true);//播放走路动画
rb.velocity = new Vector2(currentSpeed * faceDir.x * Time.deltaTime, rb.velocity.y);
}

//等待计时携程
private IEnumerator WaitTimer()
{
yield return new WaitForSeconds(waitTime); // 等待时间
transform.localScale = new Vector3(faceDir.x, transform.localScale.y, transform.localScale.z);//转向
wait = false; // 取消等待状态
}
}


## 野猪受伤死亡

public bool isHurt;//是否受伤
public float hurtForce;//击退力
public float waitHitTime = 0.5f;//受伤时长

private void FixedUpdate()
{
if(!wait && !isHurt) Move();
}

//受伤
public void OnTakeDamage(Transform attackTrans){
// attacker = attackTrans;
isHurt = true;
anim.SetTrigger("hit");

//转身面向攻击者
if (attackTrans.position.x - transform.position.x > 0)
transform.localScale = new Vector3(-Mathf.Abs(transform.localScale.x),transform.localScale.y,transform.localScale.z);
if (attackTrans.position.x - transform.position.x < 0)
transform.localScale = new Vector3(Mathf.Abs(transform.localScale.x),transform.localScale.y,transform.localScale.z);

//受伤被击退
Vector2 dir = new Vector2(transform.position.x - attackTrans.position.x, 0).normalized;

//等待切换回正常状态
StartCoroutine(OnWaitHit());
}

//等待切换回正常状态
private IEnumerator OnWaitHit()
{
yield return new WaitForSeconds(waitHitTime);
isHurt = false;
}


## 死亡

public bool isDead;//是否死亡

private void FixedUpdate()
{
if(!wait && !isHurt && !isDead) Move();
}

//死亡
//销毁
}

{
yield return new WaitForSeconds(1f);
Destroy(gameObject);
}


## 敌人死亡时，还是会对人物产生伤害

//死亡
gameObject.layer = 2;//修改图层，避免敌人死亡时，还是会对人物产生伤害
//销毁
}


## 有限状态机&抽象类多态 定义不同状态的敌人行为

public abstract class BaseState
{
protected Enemy currentEnemy; // 当前敌人
public abstract void OnEnter(Enemy enemy); // 进入状态时的方法
public abstract void LogicUpdate(); // 逻辑更新方法
public abstract void PhysicsUpdate(); // 物理更新方法
public abstract void OnExit(); // 退出状态时的方法
}


• 在OnEnter方法中，初始化当前敌人（野猪）对象。
• LogicUpdate方法中，根据敌人面朝方向和是否撞墙来切换敌人状态。
• PhysicsUpdate方法中，执行物理更新的逻辑。
• OnExit方法中，处理退出状态时的逻辑。
public class BoarPatrolState : BaseState
{
public override void OnEnter(Enemy enemy)
{
currentEnemy = enemy;
}

public override void LogicUpdate()
{
//按敌人面向和撞墙 切换敌人状态
if (currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDir.x < 0 || currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x > 0)
{
currentEnemy.wait = true; // 设为等待状态
currentEnemy.anim.SetBool("walk", false);//禁止走路动画
}
}

public override void PhysicsUpdate()
{

}

public override void OnExit()
{
currentEnemy.anim.SetBool("walk", false);
}
}


public bool isWaitTimer;//是否开始等待计时

private BaseState currentState;// 当前状态
protected BaseState patrolState;// 巡逻状态
protected BaseState ChaseState;// 追逐状态

//...

private void OnEnable()
{
currentState = patrolState;
currentState.OnEnter(this);
}

private void Update()
{
//面向方向 默认右边为正方向
faceDir = new Vector3(-transform.localScale.x, 0, 0);
currentState.LogicUpdate();
}

private void FixedUpdate()
{
if (!wait && !isHurt && !isDead) Move();

currentState.PhysicsUpdate();

if (wait && !isWaitTimer)
{
isWaitTimer = true;
StartCoroutine(WaitTimer()); // 启动等待计时
}
}

private void OnDisable()
{
currentState.OnExit();
}

//...



public class Boar : Enemy {
protected override void Awake() {
base.Awake();
patrolState = new BoarPatrolState();// 设置野猪的巡逻状态
}
}


## 防止野猪在悬崖掉下去

public override void LogicUpdate()
{
//按敌人面向和撞墙切换敌人状态
if (
!currentEnemy.physicsCheck.isGround
|| currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDir.x < 0
|| currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x > 0
)
{
currentEnemy.wait = true; // 设为等待状态
currentEnemy.anim.SetBool("walk", false);//禁止走路动画
}
else
{
currentEnemy.wait = false;
currentEnemy.anim.SetBool("walk", true);//播放走路动画
}
}


## 野猪的追击状态的转换

### 敌人主动查找玩家

[Header("主动发现玩家检测")]
public Vector2 centerOffset;//检测框的中心偏移量
public Vector2 checkSize;//检测框的尺寸
public float checkDistance;//检测的距离

//发现玩家
public bool FoundPlayer()
{
return Physics2D.BoxCast(transform.position + (Vector3)centerOffset, checkSize, 0, faceDir, checkDistance, attackLayer);
}

//在场景显示检查距离
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.red; // 设置绘制颜色为黄色，你可以根据需要选择其他颜色
Gizmos.DrawWireCube(transform.position + (Vector3)centerOffset + new Vector3(-transform.localScale.x * checkDistance, 0, 0), checkSize); // 绘制一个边框的立方体表示检测区域
}


## 追击状态

public class BoarChaseState : BaseState
{
public override void OnEnter(Enemy enemy)
{
currentEnemy = enemy;
}

public override void LogicUpdate()
{

}

public override void PhysicsUpdate()
{

}

public override void OnExit()
{

}
}


public class Boar : Enemy {
protected override void Awake() {
base.Awake();
patrolState = new BoarPatrolState();// 设置野猪的巡逻状态
chaseState = new BoarChaseState();// 设置野猪的追击状态
}
}


public enum EnemyState
{
Patrol, Chase, Skill
}


//切换敌人状态
public void SwitchState(EnemyState state)
{
var newState = state switch
{
EnemyState.Patrol => patrolState,
EnemyState.Chase => chaseState,
_ => null
};
currentState.OnExit();//退出上一个状态
currentState = newState; //赋值新状态
currentState.OnEnter(this);//开始新的状态
}


public override void LogicUpdate()
{
if(currentEnemy.FoundPlayer()){
Debug.Log("发现玩家");
currentEnemy.SwitchState(EnemyState.Chase);
}

//...
}


## 完善追击状态脚本

### 追击状态 修改速度 播放奔跑动画 敌人碰壁直接转向不等待

public class BoarChaseState : BaseState
{
public override void OnEnter(Enemy enemy)
{
currentEnemy = enemy;
currentEnemy.currentSpeed = currentEnemy.chaseSpeed;//追击速度
currentEnemy.anim.SetBool("run", true);//奔跑动画
}

public override void LogicUpdate()
{
// 如果超过等待时间，切换为默认巡逻状态
if (currentEnemy.timeSincePlayerLost >= currentEnemy.maxTimeWithoutPlayer)
{
currentEnemy.SwitchState(EnemyState.Patrol);
}

//按敌人 是否在悬崖边 面向和撞墙 切换敌人状态
if (
!currentEnemy.physicsCheck.isGround
|| currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDir.x < 0
|| currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x > 0
)
{
currentEnemy.transform.localScale = new Vector3(currentEnemy.faceDir.x, currentEnemy.transform.localScale.y, currentEnemy.transform.localScale.z);//转向
}
}

public override void PhysicsUpdate()
{

}

public override void OnExit()
{
currentEnemy.anim.SetBool("run", false);
}
}


### 野猪丢失目标，一段时间后回到默认状态

[Header("丢失目标计时器参数")]
public float lostTimeCounter = 0f;//计时器
public float lostTime = 2f; // 丢失目标时间

private void FixedUpdate()
{
//...

//计时器
Timer();
}

//追击计时器
private void Timer()
{
// 如果发现玩家，则重置计时器
if (FoundPlayer())
{
lostTimeCounter = 0f;
}
else
{
if (lostTimeCounter >= lostTime)
{
lostTimeCounter = lostTime;
}
else
{
lostTimeCounter += Time.deltaTime;
}
}
}


public override void LogicUpdate()
{
// 如果超过等待时间，切换为默认巡逻状态
if (currentEnemy.lostTimeCounter >= currentEnemy.lostTime)
{
currentEnemy.SwitchState(EnemyState.Patrol);
}

//...
}


public override void OnEnter(Enemy enemy)
{
currentEnemy = enemy;
currentEnemy.currentSpeed = currentEnemy.normalSpeed;
}


## 野猪朝我们冲锋时，正面受到攻击 无法击退 背面受到攻击又会击退很远

//受伤
public void OnTakeDamage(Transform attackTrans)
{
// 。。。

//受伤被击退
Vector2 dir = new Vector2(transform.position.x - attackTrans.position.x, 0).normalized;
rb.velocity = new Vector2(0, rb.velocity.y);//先取消刚体x轴的力

//等待切换回正常状态
StartCoroutine(OnWaitHit());
}


## 参考

https://space.bilibili.com/370283072

|
15天前
|

【8月更文挑战第31天】本文是 Unity 2D 游戏开发的全面指南，手把手教你从零开始构建精美的平面冒险游戏。首先，通过 Unity Hub 创建 2D 项目并导入游戏资源。接着，编写 PlayerController 脚本来实现角色移动，并添加动画以增强视觉效果。最后，通过 Collider 2D 组件实现碰撞检测等游戏机制。每一步均展示 Unity 在 2D 游戏开发中的强大功能。
55 6
|
15天前
|

【9月更文挑战第1天】在开发游戏时，Unity 引擎让创意变为现实。但软件开发中难免遇到 Bug，若不解决，将严重影响用户体验。调试与测试成为确保游戏质量的最后一道防线。本文介绍如何利用 Unity 的调试工具高效排查问题，并通过 Profiler 分析性能瓶颈。此外，Unity Test Framework 支持自动化测试，提高开发效率。结合单元测试与集成测试，确保游戏逻辑正确无误。对于在线游戏，还需进行压力测试以验证服务器稳定性。总之，调试与测试贯穿游戏开发全流程，确保最终作品既好玩又稳定。
40 4
|
15天前
|

【8月更文挑战第31天】游戏的加载时间是影响玩家初次体验的关键因素，特别是在移动设备上。本文介绍了几种常见的Unity游戏加载优化方法，包括资源的预加载与异步加载、使用AssetBundles管理动态资源、纹理和模型优化、合理利用缓存系统以及脚本优化。通过具体示例代码展示了如何实现异步加载场景，并提出了针对不同资源的优化策略。综合运用这些技术可以显著缩短加载时间，提升玩家满意度。
38 5
|
15天前
|

【独家揭秘】那些让你的游戏瞬间鲜活起来的Unity UI动画技巧：从零开始打造动态按钮，提升玩家交互体验的绝招大公开！
【9月更文挑战第1天】在游戏开发领域，Unity 是最受欢迎的游戏引擎之一，其强大的跨平台发布能力和丰富的功能集让开发者能够迅速打造出高质量的游戏。优秀的 UI 设计对于游戏至关重要，尤其是在手游市场，出色的 UI 能给玩家留下深刻的第一印象。Unity 的 UGUI 系统提供了一整套解决方案，包括 Canvas、Image 和 Button 等组件，支持添加各种动画效果。
37 3
|
15天前
|

【9月更文挑战第1天】游戏开发中的架构设计是项目成功的关键。良好的架构能提升开发效率并确保项目的长期可维护性和可扩展性。在使用Unity引擎时，合理的架构尤为重要。本文探讨了如何在Unity中实现可扩展且易维护的游戏架构，包括模块化设计、使用脚本对象管理数据、应用设计模式（如状态模式）及采用MVC/MVVM架构模式。通过这些方法，可以显著提高开发效率和游戏质量。例如，模块化设计将游戏拆分为独立模块。
41 3
|
15天前
|

【8月更文挑战第31天】Unity地形编辑器是游戏开发中的重要工具，可快速创建复杂多变的游戏环境。本文通过比较不同地形编辑技术，详细介绍如何利用其功能构建广阔且精细的游戏世界，并提供具体示例代码，展示从基础地形绘制到植被与纹理添加的全过程。通过学习这些技巧，开发者能显著提升游戏画面质量和玩家体验。
48 3
|
15天前
|

Unity插件开发全攻略：从零起步教你用C++扩展游戏功能，解锁Unity新玩法的详细步骤与实战技巧大公开
【8月更文挑战第31天】Unity 是一款功能强大的游戏开发引擎，支持多平台发布并拥有丰富的插件生态系统。本文介绍 Unity 插件开发基础，帮助读者从零开始编写自定义插件以扩展其功能。插件通常用 C++ 编写，通过 Mono C# 运行时调用，需在不同平台上编译。文中详细讲解了开发环境搭建、简单插件编写及在 Unity 中调用的方法，包括创建 C# 封装脚本和处理跨平台问题，助力开发者提升游戏开发效率。
28 0
|
15天前
|
vr&ar 图形学 API
Unity与VR控制器交互全解：从基础配置到力反馈应用，多角度提升虚拟现实游戏的真实感与沉浸体验大揭秘
【8月更文挑战第31天】虚拟现实（VR）技术迅猛发展，Unity作为主流游戏开发引擎，支持多种VR硬件并提供丰富的API，尤其在VR控制器交互设计上具备高度灵活性。本文详细介绍了如何在Unity中配置VR支持、设置控制器、实现按钮交互及力反馈，结合碰撞检测和物理引擎提升真实感，助力开发者创造沉浸式体验。
34 0
|
15天前
|

【独家揭秘】Unity游戏开发秘籍：从基础到进阶，掌握材质与纹理的艺术，打造超现实游戏视效的全过程剖析——案例教你如何让每一面墙都会“说话”
【8月更文挑战第31天】Unity 是全球领先的跨平台游戏开发引擎，以其高效性能和丰富的工具集著称，尤其在提升游戏视觉效果方面表现突出。本文通过具体案例分析，介绍如何利用 Unity 中的材质与纹理技术打造逼真且具艺术感的游戏世界。材质定义物体表面属性，如颜色、光滑度等；纹理则用于模拟真实细节。结合使用两者可显著增强场景真实感。以 FPS 游戏为例，通过调整材质参数和编写脚本动态改变属性，可实现自然视觉效果。此外，Unity 还提供了多种高级技术和优化方法供开发者探索。
33 0
|
15天前
|

【8月更文挑战第31天】音频设计在游戏开发中至关重要，不仅能增强沉浸感，还能传递信息，构建氛围。Unity作为跨平台游戏引擎，提供了丰富的音频处理功能，助力开发者轻松实现复杂音效。本文将探讨如何利用Unity的音频设计提升游戏氛围，并通过具体示例代码展示实现过程。例如，在恐怖游戏中，阴森的背景音乐和突然的脚步声能增加紧张感；在休闲游戏中，轻快的旋律则让玩家感到愉悦。
30 0