unity 3D RPG教程(四)

目录

声明

16:AttackData 攻击属性

17:Execute Attack 实现攻击数值计算

18:Guard & Dead 守卫状态和死亡状态

19:泛型单例模式 Singleton

20:Observer Pattern 接口实现观察者模式的订阅和广播


声明

本教程学习均来自U3D中文课堂麦扣老师

16:AttackData 攻击属性

将人物攻击的数值也写成ScriptObject,在Scripts文件夹中创建一个与攻击相关的文件夹Combat,创建脚本AttactData_SO:写一些基本的数值

AttactData_SO:

[CreateAssetMenu(fileName = "New Attack",menuName ="Attack/Attack Data")]
public class AttactData_SO : ScriptableObject
{
    public float attackRange;//攻击距离

    public float skillRange;//远程攻击距离

    public float coolDown;//冷却时间

    public float minDamge;//最小攻击数值

    public float maxDamge;//最大攻击数值

    public float criticalMultiplier;//暴击加成

    public float criticalChance;//暴击率
}

创建PlayerBaseAttack Data ,设置数值

 创建好了需要在CharacterStats代码中进行调用,这样人物可以读到攻击数值:

 创建 AttactData_SO变量即可:   public AttactData_SO attactData;

在PlayerController中补充未做的修改攻击范围参数:

 //修改攻击范围参数
while(Vector3.Distance(attackTarget.transform.position,transform.position)>characterStats.attactData.attackRange)
{
  agent.destination = attackTarget.transform.position;
  yield return null;
}

  在EnemyController中完善Chase状态:在攻击范围内则攻击

    先定义CharacterStats 变量,     private CharacterStats characterStats;

    在Awake中初始化:characterStats = GetComponent<CharacterStats>();

写2个方法判断是否在攻击范围内:

    bool TargetInAttackRange()//是否能进行近距离攻击
    {
        if (attackTarget != null)
            return Vector3.Distance(attackTarget.transform.position, transform.position) <= characterStats.attactData.attackRange;
        else 
            return false;
    }

    bool TargetInSkillRange()//是否能进行远距离攻击
    {
        if (attackTarget != null)
            return Vector3.Distance(attackTarget.transform.position, transform.position) <= characterStats.attactData.skillRange;
        else
            return false;
    }

    定义攻击时间间隔:   private float lastAttackTime;

在Update中进行计时:    lastAttackTime -= Time.deltaTime;

                //TODO: 在攻击范围内则攻击
                if(TargetInAttackRange()||TargetInSkillRange())//在攻击范围内
                {
                    isFollow = false;
                    agent.isStopped = true;

                    if(lastAttackTime < 0)//攻击
                    {
                        lastAttackTime = characterStats.attactData.coolDown;
                        //暴击判断

                    }
                }

进行暴击判断:在CharacterStats创建一个布尔值判断是否暴击:public bool isCritical;上一行加上  [HideInInspector]让它不在Inspector面板显示

回到EnemyController:

进行暴击判断:

//暴击判断
 characterStats.isCritical = Random.value < characterStats.attactData.criticalChance;

写一个执行攻击的方法:

    void Attack()//执行攻击
    {
        transform.LookAt(attackTarget.transform);//看着攻击目标
        if(TargetInAttackRange())
        {
            //近身攻击动画
        }
        if(TargetInSkillRange())
        {
            //技能攻击动画
        }
    }

 为Slime添加Attack Data:

添加攻击动画:

敌人跑到人物Player面前要停止移动:回到IdleBattle的状态下,然后进行暴击攻击和普通攻击的判断:

    void SwitchAnimation()//切换动画
    {
        anim.SetBool("Walk",isWalk);
        anim.SetBool("Chase",isChase);
        anim.SetBool("Follow",isFollow);
        anim.SetBool("Critical", characterStats.isCritical);
    }

    void Attack()//执行攻击
    {
        transform.LookAt(attackTarget.transform);//看着攻击目标
        if(TargetInAttackRange())
        {
            //近身攻击动画
            anim.SetTrigger("Attack");
        }
        if(TargetInSkillRange())
        {
            //技能攻击动画
            anim.SetTrigger("Skill");
        }
    }

现在敌人可以攻击了,但是拉托敌人后敌人不动了,要把 agent.isStopped = false;加上让敌人可以继续移动

                if(!FoundPlayer())
                {
                    //拉托回到上一个状态
                    isFollow = false;
                    if(remainLookAtTime > 0)
                    {
                        agent.destination = transform.position;
                        remainLookAtTime -= Time.deltaTime;
                    }

                    else if(isGuard)
                    {
                        enemyStates = EnemyStates.GUARD;
                    }
                    else
                    {
                        enemyStates = EnemyStates.PATROL;
                    }

                }
                else
                {
                    isFollow = true;
                    agent.isStopped = false;
                    agent.destination = attackTarget.transform.position;//追Player
                }

17:Execute Attack 实现攻击数值计算

添加Player暴击动画:

 代码当中保持同步:

        //Attack
        if(lastAttackTime < 0)
        {
            anim.SetTrigger("Attack");
            anim.SetBool("Critical", characterStats.isCritical);
            //重置冷却时间
            lastAttackTime = characterStats.attactData.coolDown;
        }

接下来就可以写受伤的计算公式了,受伤部分的计算放在CharacterStats里面,因为这里面读取了最基本的每一个人物的数值,

    #region Character Combat
    public void TakeDamage(CharacterStats attacker, CharacterStats defener)
    {
        int damage = Mathf.Max(attacker.CurrentDamage() - defener.CurrentDefence,0);//保证伤害不会是负值
        CurrentHealth = Mathf.Max(CurrentHealth - damage, 0);//保证血量不会是负值

        //TODO:Update UI
        //TODO:经验Update
    }

    private int CurrentDamage()//当前伤害
    {
        float coreDamage = UnityEngine.Random.Range(attactData.minDamge, attactData.maxDamge);//核心伤害

        if(isCritical)//暴击
        {
            coreDamage *= attactData.criticalMultiplier;
        }

        return (int)coreDamage;
    }

    #endregion

下面回到界面,

 在动画的位置执行一个事件,事件来调用一个函数方法来计算它们2个之间的生命,

先在PlayerController中补充暴击布尔值代码:

    private void EventAttack(GameObject target)
    {
        if(target != null)
        {
            attackTarget = target;
            characterStats.isCritical = UnityEngine.Random.value < characterStats.attactData.criticalChance;
            StartCoroutine(MoveToAttackTarget());//协程:攻击敌人
        }
    }

 写Hit事件:

    //Animation Event
    void Hit()
    {
        var targetStats = attackTarget.GetComponent<CharacterStats>();//临时变量

        targetStats.TakeDamage(characterStats,targetStats);
    }

添加事件: 

 

 同样,在EnemyController中添加事件方法:

    //Animation Event
    void Hit()
    {
        if(attackTarget != null)
        {
            var targetStats = attackTarget.GetComponent<CharacterStats>();//临时变量
            targetStats.TakeDamage(characterStats, targetStats);
        }
    }

发现敌人的动画都是Read——Only的 

 如何解决FBX导入动画无法编辑的问题:将FBX文件里的动画Ctrl+D复制一份出来并更换掉之前的就可以了

 现在就可以添加事件了

现在就可以造成伤害了

18:Guard & Dead 守卫状态和死亡状态

ScriptableObject一个特性就是在你打包好了游戏后只要游戏不退出,ScriptableObject也会一直保留你的数据,所以每次试玩结束后要记得把数值该回去

进入EnemyController当中写一下另外的几个状态:

            case EnemyStates.GUARD:
                isChase = false;
                if(transform.position != guardPos)
                {
                    isWalk = true;
                    agent.isStopped = false;
                    agent.destination = guardPos;//回到站桩点
                    if(Vector3.SqrMagnitude(guardPos - transform.position)<= agent.stoppingDistance)//判断两点之间的距离
                    {
                        isWalk = false;
                    }
                }

要使得Slime回去能转向回原来的方向,创建一个四元数变量来存储原来的rotation

                                  private Quaternion guardRotation;//初始旋转角度

在Awake中初始化: guardRotation = transform.rotation;

缓慢转回原来的角度:

            case EnemyStates.GUARD:
                isChase = false;
                if(transform.position != guardPos)
                {
                    isWalk = true;
                    agent.isStopped = false;
                    agent.destination = guardPos;//回到站桩点
                    if(Vector3.SqrMagnitude(guardPos - transform.position)<= agent.stoppingDistance)//判断两点之间的距离
                    {
                        isWalk = false;
                        transform.rotation = Quaternion.Lerp(transform.rotation, guardRotation, 0.01f);
                    }
                }

添加死亡动画:

为Slime创建一个Death Layer,

 同样给Player添加动画:

 先来制作Enemy死亡的有关部分:

创建bool变量  bool isDead;

Update中实时监测:

        if(characterStats.CurrentHealth == 0)
        {
            isDead = true;//死亡
        }

 SwitchAnimation()切换动画方法中加入 anim.SetBool("Death", isDead);

    void SwitchStates() //切换状态
    {
        if (isDead)//死亡
            enemyStates = EnemyStates.DEAD;

        //如果发现Player,切换为CHASE
        else if(FoundPlayer())
        {
            enemyStates = EnemyStates.CHASE;
        }
       。。。

补充死亡状态:

 case EnemyStates.DEAD:
          agent.enabled = false;

          Destroy(gameObject, 2f);
   break;

现在就可以杀死敌人了:但是敌人死了之后还能攻击,我们可以消除敌人的Collider

定义   private Collider coll;

Awake初始化:   coll = GetComponent<Collider>();

            case EnemyStates.DEAD:
                coll.enabled = false;
                agent.enabled = false;
                Destroy(gameObject, 2f);
                break;

现在就可以了

下面设置Player的死亡状态:    private bool isDead;

Update:   isDead = characterStats.CurrentHealth == 0;

    private void SwitchAnimation()//实时切换动画
    {
        anim.SetFloat("Speed", agent.velocity.sqrMagnitude);    //sqrMagnitude将velocity转换为浮点数值
        anim.SetBool("Death", isDead);
    }

就完成死亡状态了;

19:泛型单例模式 Singleton

创建GameManager

public class GameManager : MonoBehaviour
{
    public CharacterStats playerStats;

    public void RigisterPlayer(CharacterStats player)//反向注册
    {
        playerStats = player;
    }
}

泛型单例模式 Singleton:创建一个文件夹和脚本

打开Singleton:

泛型类的创建方法:在一个类名后面接上一个尖括号,尖括号里通常写的是它的类型

对MonoBehaviour做一个约束,代表它是Singleton的一个类型:

public class Singleton<T>: MonoBehaviour where T: Singleton<T>
{

}

 这是通常泛型单例的写法,

然后可以将MouseManager当中的初始的Awake方法和创建static静态变量的方法都挪到Singleton里面来写

Singleton:

public class Singleton<T>: MonoBehaviour where T: Singleton<T>
{
    private static T instance;

    public static T Instance
    {
        get{ return instance; }
    }

    protected virtual void Awake()
    {
        if(instance != null)
        {
            Destroy(gameObject);
        }
        else
        {
            instance = (T)this;
        }
    }

    public static bool IsInitialized//判断当前单例模式是否已经初始化生成了
    {
        get { return instance != null; }
    }

    protected virtual void OnDestroy()//销毁单例
    {
        if(instance == this)
        {
            instance = null;
        }
    }
}

改一下MouseManager和GameManager:

public class MouseManager : Singleton<MouseManager>
public class GameManager : Singleton<GameManager>

这样就能直接用了。

接下回到PlayerController注册:

    private void Start()
    {
        。。。

        GameManager.Instance.RigisterPlayer(characterStats);//注册GameManager
    }

20:Observer Pattern 接口实现观察者模式的订阅和广播

创建脚本使用第一个接口IEndGameObserver:当结束游戏给这些观察者们来调用的方法

public interface IEndGameObserver 
{

}

我们要实现的就是接口需要调用的函数方法,在这里我们只写方法的定义,而不写方法里面的实现,也就是接口当中我们只写每一个使用了这个接口的代码一定要调用的一些函数

比如:

public interface IEndGameObserver 
{
    void EndNotify();//结束游戏的广播
}
public class EnemyController : MonoBehaviour,IEndGameObserver

修补接口可以看到:所有调用了这个接口都一定要有这个函数方法在代码当中 

 每一个调用接口的代码才写函数里面具体的方法

那么怎么做观察者模式的订阅和广播呢?以后我们会生成多个不同的敌人,每一个敌人都有这个接口,那么我们可以创建一个列表在GameManager当中去收集所有加载了这个接口的函数方法,那就代表它是一个敌人,需要订阅我的结束游戏的广播

在GameManager中创建一个接口类型的列表:用注册的方式让这些观察者主动添加到我们列表当中,敌人生成的时候添加到列表,死亡的时候从列表删除

public class GameManager : Singleton<GameManager>
{
    public CharacterStats playerStats;

    List<IEndGameObserver> endGameObservers = new List<IEndGameObserver>();//观察者列表

    public void RigisterPlayer(CharacterStats player)//反向注册
    {
        playerStats = player;
    }

    public void AddObserver(IEndGameObserver observer)//让观察者主动添加到列表
    {
        endGameObservers.Add(observer);
    }

    public void RemoveObserver(IEndGameObserver observer)//让观察者移除列表
    {
        endGameObservers.Remove(observer);
    }
}

 EnemyController:

    private void OnEnable()
    {
        GameManager.Instance.AddObserver(this);//让观察者主动添加到列表
    }

    private void OnDisable()//销毁完成之后执行
    {
        GameManager.Instance.RemoveObserver(this);//让观察者移除列表
    }

在GameManager中实现广播:

    public void NotifyObservers()//向所有的观察者广播
    {
        foreach(var observer in endGameObservers)
        {
            observer.EndNotify();
        }
    }

当Player死亡的时候调用这个功能广播:

PlayerController:

    private void Update()
    {
        isDead = characterStats.CurrentHealth == 0;

        if(isDead)
        {
            GameManager.Instance.NotifyObservers();//广播
        }

        SwitchAnimation();//实时切换动画
        lastAttackTime -= Time.deltaTime;
    }

最后在EnemyController当中实现广播内容:

添加Slime胜利动画在Victory Layer上

 现在就实现实现 Player 死亡敌人集体欢呼胜利了

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
Unity3D RPG游戏框架是一种用于开发角色扮演游戏的一套工具和框架。它提供了一系列功能和组件,使开发者能够快速建立一个具有角色控制、任务系统、战斗系统等功能的游戏。 首先,Unity3D RPG游戏框架提供了强大的角色控制功能。开发者可以轻松地创建角色,并对其进行动画、物理和碰撞等控制。框架还提供了角色属性和状态管理的机制,使开发者能够定义和管理角色的生命值、能力值和状态等。 其次,Unity3D RPG游戏框架支持任务系统的开发。开发者可以创建各种类型的任务,如主线任务、支线任务和日常任务等,并为每个任务定义任务目标、奖励和任务进度等。框架还提供了任务的触发和完成的事件回调,使开发者能够灵活地控制任务的逻辑和流程。 此外,Unity3D RPG游戏框架还包括战斗系统的实现。开发者可以创建各种类型的敌人和怪物,并为其定义属性、技能和行为等。框架提供了各种战斗机制,如近战攻击、远程攻击和技能释放等。同时,框架还支持战斗AI的设计和开发,使敌人和怪物能够智能地进行战斗。 此外,Unity3D RPG游戏框架还提供了一些额外的功能和工具,如用户界面、物品系统和声音管理等。开发者可以使用这些功能来增强游戏的可玩性和趣味性。 总之,Unity3D RPG游戏框架是一个功能强大的工具和框架,它能够帮助开发者快速建立一个完整的RPG游戏。无论是开发者的经验水平还是游戏的规模,都可以借助这个框架来实现自己的创意和想法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值