Unity百游修炼23——C#和Unity实现2D完整小项目《露娜历险记》(含完整项目流程)

一、项目申明

        本博客使用的所有素材(包括但不限于图片、视频、音频、文字等)已购买版权,仅供个人学习、交流、参考和研究使用,同学们如果想要用于个人项目或商业游戏请到https://itch.io/购买版权进行使用,否则需承担因使用该素材而产生的全部责任。本博客不承担任何直接或间接的法律责任。


二、效果展示

露娜


三、场景搭建

1.拖入地图素材

注意:

        MapW的层次顺序为:0

        MapH的层次顺序为:7             (为了能遮挡人物,所以图层顺序大一点)

2.拖入主角人物素材

(1)找到如图红色标注的素材,拖入Hierarchy视图中

(2)命名为Player,复制一下,让两个变为父子关系,同时删除父物体的Sprite Render组件,修改子物体的名字为为Texture

(3)调整Texture的位置,使得子物体的脚底位置恰好是父物体的轴心点位置,如图所示:

(4)Texture的图层顺序为2

(5)主角父物体的tag标签修改为 Player

3.拖入小狗素材

(1)将制定素材拖入场景中

(2)个性化放置到合适位置即可

(3)图层顺序为2

4.拖入马匹的素材

(1)将制定素材拖入场景中

(2)个性化放置到合适位置即可

 (3)图层顺序为2

5.拖入商人素材

(1)将制定素材拖入场景中

(2)个性化放置到合适位置即可

 (3)图层顺序为2


四、控制主角人物的移动

1.创建脚本文件并编辑代码

(1)在Project窗口的Asset下创建脚本文件夹Scripts

(2)在脚本文件夹下创建名为 PlayerControl的脚本

(3)挂载脚本到Player上,并给Player添加运动必备组件碰撞器Collider刚体Rigidbody

(4)双击脚本,打开脚本,编辑代码

using System.Collections;
using System.Collections.Generic;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;

public class PlayerControl : MonoBehaviour
{
    static private string x = "Horizontal";
    static private string y = "Vertical";
    private float xValue = 0;
    private float yValue = 0;
    private Rigidbody2D PlayerRig;
    
    public float moveSpeed = 100f;
    private int maxHp = 100;
    private int nowHp = 100;

    public int MaxHp {  get { return maxHp; } }
    public int NowHp {  get { return nowHp; } }

    private void Start()
    {
        PlayerRig=GetComponent<Rigidbody2D>();
    }

    private void Update()
    {
        xValue = Input.GetAxis(x);
        yValue = Input.GetAxis(y);
        Vector2 newPosition = new Vector2(this.transform.position.x + xValue * moveSpeed*Time.deltaTime, this.transform.position.y + yValue * moveSpeed * Time.deltaTime);
        PlayerRig.MovePosition(newPosition);
    }

    public void ChangeHP(int change)
    {
        nowHp = Mathf.Clamp(nowHp+change,0,maxHp);
        Debug.Log(nowHp+"/"+maxHp);
    }
}

2.修改Player相关组件的参数

(1)角色控制脚本的相关参数(个性化调整即可)

(2)碰撞器的大小设置,如图即可(个性化设置,差不多即可)

(3)刚体设置

        取消重力和冻结Z轴旋转

3.测试运行(调整到适合速度即可)

        WASD控制移动


五、预制体以及相应脚本的创建

1.地图物体——蜡烛预制体的创建

(1)将Candle素材拖入场景中

(2)修改图层顺序为2,修改大小为1.5倍

(3)将场景中的蜡烛再拖回Assets下的Prefabs文件夹,变成预制体

(4)在场景中创建空物体Candles,利用预制体创建五个蜡烛,并作为Candles的子物体进行统一管理(蜡烛位置随意放置,后期作为收集品可以收集)

2.地图物体——血瓶的创建以及血量变化脚本

(1)拖入血瓶素材并添加碰撞器,勾选触发器

(2)创建血瓶脚本,接触产生血量变化等

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

public class HpBottle : MonoBehaviour
{
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.tag=="Player")
        {
            PlayerControl playerControl = collision.GetComponent<PlayerControl>();
            Debug.Log(playerControl.NowHp);
            Debug.Log(playerControl.MaxHp);
            if (playerControl.NowHp<playerControl.MaxHp)
            {
                playerControl.ChangeHP(10);
                Destroy(gameObject);
            }
        }
    }
}


六、实体化游戏世界地形

1.创建空物体并挂载多边形碰撞体

(1)创建一个父物体空物体MapColiders统一管理

(2)创建一个子物体空物体MapC1,并添加Polygon Colider 2D

2.根据地图绘制地形碰撞体


七、动画的设计与添加

1.主角动画的创建和动画生成树创建和设计

(1)选中相应的动画素材,拖入到Animation窗口中创建动画

(2)创建Animation和Animator文件夹用来分类管理动画

(3)在Texture上创建Animator组件,并挂在人物的动画控制器

(4)进入Animator窗口,右键创建生成树,进行分类命名(Idle,Walk等等)

(5)进入Idle生成树里面,修改为2D方向,添加八个方向,并把创建的相应动画添加到相应位置

(6)创建三个float参数,一个bool参数用来控制动画之间的过度条件

(7)Walk,Run等生成树动画如上进行操作

2.主角动画的过渡条件

(1)Idle——Walk

(2)Walk——Idle

(3)Walk——Run

(4)Run——Walk

(5)生成树Idle设计

(6)生成树Run,Walk同(5)

3.主角动画代码控制设计

(1)需求分析

  1. 方向一致性:待机时保持最后移动方向(如向左走后停止,待机动画仍朝左),而非默认朝一个方向;
  2. 状态区分:支持 “待机(8 方向)→ 步行(8 方向)→ 跑步(8 方向)” 三状态切换,且跑步需通过 Shift 键触发;
  3. 参数联动:动画状态与角色移动逻辑强关联(如停止移动时立即切换待机,按下 Shift 时同步切换跑步动画)。

(2)核心变量设计

变量类型变量名作用说明关联 Animator 参数
Animatoranimator引用主角的 Animator 组件,用于传递动画参数-
Vector2LookDirection记录主角朝向(归一化向量),控制 8 方向动画LookX、LookY(float 类型)
Vector2newPosition记录当前输入的移动向量,判断是否处于移动状态MoveValue(float 类型)
boolisRunning标记是否处于跑步状态(Shift 键触发)IsRunning(bool 类型)
floatmoveSpeed/runSpeed步行 / 跑步速度,控制移动逻辑与动画节奏匹配-

(3)关键逻辑实现

【1】初始化模块(Start ())

private void Start()
{
    // 绑定移动组件(Rigidbody2D)
    PlayerRig = GetComponent<Rigidbody2D>();
    // 绑定动画组件(Animator),支持子物体查找
    animator = GetComponentInChildren<Animator>();
}

【2】输入检测与参数传递模块(Update ())

        通过 Unity 的Input.GetAxis()获取键盘或手柄的移动输入,xValue对应左右方向,yValue对应上下方向(值范围:-1~1)

        读取移动输入:获取 Horizontal/Vertical 轴值

xValue = Input.GetAxis(x); // 等价于Input.GetAxis("Horizontal")
yValue = Input.GetAxis(y); // 等价于Input.GetAxis("Vertical")

        只有当输入值超过阈值(0.1f,过滤微小误触)时,才更新LookDirection(归一化后的朝向向量)。停止移动后,LookDirection保持最后一次的朝向,从而实现 “8 方向待机”。

        更新朝向:确保待机时方向不丢失

if (Mathf.Abs(xValue) > 0.1f || Mathf.Abs(yValue) > 0.1f)
{
    // 归一化:确保斜向移动速度与横向/纵向一致(避免斜向跑得更快)
    LookDirection = new Vector2(xValue, yValue).normalized;
}

        通过Input.GetKey()检测左右 Shift 键,标记isRunning状态。只有在有移动输入时(newPosition.magnitude > 0.1f),跑步状态才生效,避免 “站立时按 Shift 触发跑步动画” 的逻辑错误。

        检测跑步状态:Shift 键触发

// 检测Shift键输入(支持左右Shift)
isRunning = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
// 记录当前移动向量(未归一化,用于判断是否处于移动状态)
newPosition = new Vector2(xValue, yValue);

(4)传递参数到 Animator:驱动动画状态切换

        通过animator.SetXXX()方法传递到 Animator 窗口,直接控制动画状态:

  • MoveValue:传递移动向量的长度(0~√2),判断 “待机”(0)或 “移动”(>0);
  • LookX/LookY:传递朝向向量的 x/y 分量(-1~1),控制 8 方向动画(如向左时 LookX=-1,向上时 LookY=1);
  • IsRunning:传递跑步状态(true/false),控制 “步行” 与 “跑步” 动画切换。
animator.SetFloat("MoveValue", newPosition.magnitude);
animator.SetFloat("LookX", LookDirection.x);
animator.SetFloat("LookY", LookDirection.y);
// 只有移动时,跑步状态才生效
animator.SetBool("IsRunning", isRunning && newPosition.magnitude > 0.1f);

(5)移动逻辑模块(FixedUpdate ())

private void FixedUpdate()
{
    // 根据跑步状态选择当前速度
    float currentSpeed = isRunning ? runSpeed : moveSpeed;
    // 控制Rigidbody2D移动(与动画状态同步)
    PlayerRig.velocity = newPosition * currentSpeed * Time.deltaTime / Time.fixedDeltaTime;
}

(6)完整代码

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

public class PlayerControl : MonoBehaviour
{
    static private string x = "Horizontal";
    static private string y = "Vertical";
    private float xValue = 0;
    private float yValue = 0;
    private Rigidbody2D PlayerRig;
    private Animator animator;

    public float moveSpeed = 3f;          // 步行速度
    public float runSpeed = 5f;          // 跑步速度
    private int maxHp = 100;
    private int nowHp = 100;
    private bool isRunning = false;       // 跑步状态标记

    public int MaxHp { get { return maxHp; } }
    public int NowHp { get { return nowHp; } }
    public Vector2 LookDirection = Vector2.zero;
    public Vector2 newPosition = Vector2.zero;

    private void Start()
    {
        PlayerRig = GetComponent<Rigidbody2D>();
        animator = GetComponentInChildren<Animator>();
    }

    private void Update()
    {
        xValue = Input.GetAxis(x);
        yValue = Input.GetAxis(y);

        // 检测移动输入并更新朝向
        if (Mathf.Abs(xValue) > 0.1f || Mathf.Abs(yValue) > 0.1f)
        {
            LookDirection = new Vector2(xValue, yValue).normalized;
        }

        // 检测Shift键输入判断是否跑步
        isRunning = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);

        newPosition = new Vector2(xValue, yValue);

        // 向动画系统传递参数
        animator.SetFloat("MoveValue", newPosition.magnitude);
        animator.SetFloat("LookX", LookDirection.x);
        animator.SetFloat("LookY", LookDirection.y);
        animator.SetBool("IsRunning", isRunning && newPosition.magnitude > 0.1f); // 只有移动时才可能跑步
    }

    private void FixedUpdate()
    {
        // 根据是否跑步选择不同的速度
        float currentSpeed = isRunning ? runSpeed : moveSpeed;
        PlayerRig.velocity = newPosition * currentSpeed * Time.deltaTime / Time.fixedDeltaTime;
    }

    public void ChangeHP(int change)
    {
        nowHp = Mathf.Clamp(nowHp + change, 0, maxHp);
        Debug.Log(nowHp + "/" + maxHp);
    }
}

4.怪兽动画的创建和动画生成树的创建和设计

        步骤和操作同主角,不过多赘述

5.怪兽动画的过渡条件

6.怪兽动画代码控制设计

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

public class EnemyControl : MonoBehaviour
{
    public bool vertical;
    public float speed = 5;
    public float changeTime = 5;

    private int direction = -1;
    private float timer;
    private Rigidbody2D rigidbody2d;
    private Animator animator;

    private void Start()
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
        timer=changeTime;
    }

    private void Update()
    {
        timer-=Time.deltaTime;
        if(timer<=0)
        {
            direction=-direction;
            timer=changeTime ;
        }
    }

    private void FixedUpdate()
    {
        Vector3 pos=rigidbody2d.position;
        if(vertical)
        {
            animator.SetFloat("LookX", 0);
            animator.SetFloat("LookY", direction);
            pos.y = pos.y+speed*direction*Time.fixedDeltaTime;
        }
        else
        {
            animator.SetFloat("LookX", direction);
            animator.SetFloat("LookY", 0);
            pos.x = pos.x + speed * direction * Time.fixedDeltaTime;
        }
        rigidbody2d.MovePosition(pos);
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.CompareTag("Player"))
        {
            GameManager.Instance.EnterOrExitBattle(true);
        }
    }
}


八、摄像机镜头跟随

1.导入插件Cinemachine

2.创建虚拟摄像机并设置参数

(1)创建虚拟摄像机

(2)设置跟随对象和视角大小

(3)添加边界组件,设置地图边界(避免视角包含地图外的场景)


九、攀爬区域的设计

1.攀爬动画的设计

        参考第七章的人物动画设计与添加

2.攀爬区域的绘制

(1)创建空物体,添加多边形碰撞器,勾选触发器,进行攀爬区域的绘制

(2)创建一个父物体统一管理攀爬区域

3.攀爬区域的代码设计

(1)修改玩家控制代码,确保玩家触发区域可以执行相应的函数

......
public void SetClimb(bool IsClimb)
{
    animator.SetBool("IsClimb", IsClimb);
}
......

(2)创建攀爬区域的脚本代码——ClimbArea

        玩家触发和离开区域执行相应的代码逻辑

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

public class ClimbArea : MonoBehaviour
{
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.tag=="Player")
        {
            collision.GetComponent<PlayerControl>().SetClimb(true);
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.tag == "Player")
        {
            collision.GetComponent<PlayerControl>().SetClimb(false);
        }
    }
}

(4)将脚本挂载到攀爬区域物体上即可


十、UI设计

1.头像UI设计

(1)创建画布,画布UI模式设置为大小根据屏幕大小变化

(2)创建一个image用来存放头像,锚点设置为右上角

(3)再创建一个Image用来存放头像外边框,锚点设置为如图

2.血条和蓝条设计

(1)创建image_Hp,拖入血条背景

(2)创建image_Red,拖入血条素材,改变颜色

(3)创建image_Red的空父物体,添加image和mask组件,拖入mask素材

(4)复制一下,修改颜色为蓝色,即为蓝条

3.血条和蓝条的代码设计

(1)核心变量设计

  • 单例模式(Instance):通过Awake()方法初始化单例,确保全局只有一个UIManager实例,方便其他脚本(如角色控制器)随时调用血条更新方法。
  • UI 元素引用(HpImage、MpImage):通过public Image类型变量关联场景中的血条和蓝条图片组件,这两个Image通常在 Unity 编辑器中分别赋值为 “血条填充图” 和 “蓝条填充图”。

(2)血条更新逻辑

        根据当前血量 / 蓝量与最大值的比例,动态调整填充图片的宽度

// 调整血条填充图的水平宽度
HpImage.rectTransform.SetSizeWithCurrentAnchors(
    RectTransform.Axis.Horizontal,  // 调整水平方向尺寸
    HpOrMax * HpImage.rectTransform.rect.width  // 计算新宽度:比例 × 原始宽度
);

(3)完整的代码 UIManager

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

public class UIManager : MonoBehaviour
{
    private UIManager Instance;
    public Image HpImage;
    public Image MpImage;

    private void Awake()
    {
        Instance = this;
    }

    public void SetHpValue(float HpOrMax)//设置血条
    {
        HpImage.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, HpOrMax * HpImage.rectTransform.rect.width);
    }

    public void SetMpValue(float HpOrMax)//设置蓝条
    {
        HpImage.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, HpOrMax * HpImage.rectTransform.rect.width);
    }
}


十一、战斗场景的制作

1.游戏管理器以及脚本的制作

(1)创建空物体GameManager

(2)创建脚本GameManager并挂在到该空物体上

(3)修改PlayerControl代码,把上面的游戏场景相关代码迁移到GameManager脚本中

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

public class GameManager : MonoBehaviour
{
    public static GameManager Instance;
    private int maxHp = 100;
    private int nowHp = 50;

    public int MaxHp { get { return maxHp; } }
    public int NowHp { get { return nowHp; } }

    private void Awake()
    {
        Instance = this;
    }

    public void ChangeHP(int change)
    {
        nowHp = Mathf.Clamp(nowHp + change, 0, maxHp);
        Debug.Log(nowHp + "/" + maxHp);
    }
}

2.游戏场景的制作

(1)将战斗场景素材拖拽大场景中作为摄像头的子物体,调整大小

(2)将战斗的角色和怪兽拖入到场景素材下作为子物体

3.战斗玩家以及动画创建和设计

(1)拖入战斗玩家素材

(2)根据素材创建动画并分类

(3)创建动画树和过渡条件

4.战斗怪兽以及动画创建和设计

 (1)拖入战斗玩家素材

(2)根据素材创建动画并分类

(3)创建动画

5.对战UI交互制作

(1)创建1个Image,2个Text,5个Button


十二、跳跃区域的设计

1.跳跃动画的设计

        参考第七章的人物动画设计与添加

2.跳跃区域的绘制

(1)创建一个空物体,绘制碰撞体合适大小,并创建跳跃区域脚本,并挂载上面,如图

(2)创建两个跳跃点,分别在两边

(3)多创建几个跳跃区域散布在地图需要跳跃的位置上即可

(4)安装DOTween插件

3.跳跃区域的代码设计

(1)源代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;
public class JumpArea : MonoBehaviour
{
    public Transform JumpA;
    public Transform JumpB;
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.tag == "Player")
        {
            collision.GetComponent<PlayerControl>().SetJump(true);
            float disA = Vector3.Distance(collision.GetComponent<PlayerControl>().transform.position, JumpA.position);
            float disB = Vector3.Distance(collision.GetComponent<PlayerControl>().transform.position, JumpB.position);
            Transform GoPosition=disA > disB ? JumpA : JumpB;
            collision.GetComponent<PlayerControl>().transform.DOMove(GoPosition.position, 0.5f).SetEase(Ease.Linear).OnComplete(() => { EndJump(collision.GetComponent<PlayerControl>()); });
            Transform playerTrans = collision.GetComponent<PlayerControl>().transform.GetChild(0);
            Sequence sequence = DOTween.Sequence();
            sequence.Append(playerTrans.DOLocalMoveY(1.5f,0.25f).SetEase(Ease.InOutSine));
            sequence.Append(playerTrans.DOLocalMoveY(0.61f, 0.25f).SetEase(Ease.InOutSine));
            sequence.Play();
        }
    }

    public void EndJump(PlayerControl playerControl)
    {
        playerControl.SetJump(false);
    }
}

(2)核心逻辑与方法设计

  1. 碰撞检测触发(OnTriggerEnter2D 方法)

    • 触发条件:当 2D 碰撞体(Collider2D)进入当前物体的碰撞范围时调用(需确保当前物体和玩家物体的碰撞体设置为 “触发器”,且有 2D 刚体组件)。
    • 标签判断:通过 collision.tag == "Player" 筛选碰撞对象,仅对玩家生效,避免其他物体触发跳跃逻辑。
  2. 跳跃状态控制

    • 调用玩家控制器(PlayerControl)的 SetJump(true) 方法:在跳跃开始时设置玩家为 “跳跃中” 状态(推测用于禁用玩家自主移动、防止重复触发跳跃等)。
    • 跳跃结束后通过 EndJump 方法调用 SetJump(false):恢复玩家正常状态。
  3. 跳跃位移实现

    • 使用 DOTween 插件的 DOMove 方法:实现玩家位置从当前点到 GoPosition 的平滑移动,设置移动时间为 0.5 秒,动画曲线为 Ease.Linear(线性移动,保证速度均匀)。
    • 回调函数 OnComplete:移动结束后自动调用 EndJump 方法,完成状态重置。
  4. 跳跃动画效果

    • 获取玩家子对象(playerTrans = transform.GetChild(0)):通常为玩家的视觉表现模型(如角色 sprite),用于单独控制跳跃时的上下浮动动画。
    • 序列动画(Sequence):通过 DOTween 的序列动画实现 “上升 - 下降” 的跳跃弧度:
      • 先执行 DOLocalMoveY(1.5f, 0.25f):0.25 秒内沿 Y 轴(本地坐标)上升到 1.5 位置,动画曲线为 Ease.InOutSine(平滑正弦曲线,模拟自然起跳)。
      • 再执行 DOLocalMoveY(0.61f, 0.25f):0.25 秒内下降到 0.61 位置(恢复接近初始高度),曲线同上,模拟落地效果。

十三、后续自行拓展实现,本文起到抛砖引玉的作用

十四、完整功能项目以及源代码

通过网盘分享的文件:Luna's Fantasy
链接: https://pan.baidu.com/s/1k1PTa-GXS0NI7jF7hP8wWw?pwd=1111 提取码: 1111 
--来自百度网盘超级会员v5的分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值