Unity经典游戏教程之:弓之骑士

Wesley13
• 阅读 759

版权声明:

  • 本文原创发布于博客园"优梦创客"的博客空间(网址:http://www.cnblogs.com/raymondking123/)以及微信公众号“优梦创客”(微信号:unitymaker)
  • 您可以自由转载,但必须加入完整的版权声明!

一 原始游戏

Unity经典游戏教程之:弓之骑士

原始游戏玩法:

游戏名:弓箭手。玩家控制拿着弓箭的弓箭手,玩家AD键控制弓箭手左右移动,鼠标进行射击,同时鼠标可长按进行蓄力,使得弓箭射出的速度更快,箭能射的更远。两边自动生成战士AI,左边为友方AI,右边为敌方AI,相遇时会作战,玩家带领友方AI进攻至最右边方可胜利,同样的,敌方AI进攻至最左边时则游戏失败。

Unity经典游戏教程之:弓之骑士

参考游戏玩法:

游戏名:战争进化史。玩家点击右上角按钮,花费金币购买兵种进行战斗,攻破敌方城堡即为胜利。

二 改进的个人项目玩法

本游戏(暂时)有三个场景,分别为Castle场景、Battle场景和Boss战场景。

在Castle场景中,玩家操控主角在王国中行走,可以通过房子的们进入房子,当前开发了训练室和练兵场两块场地,进入后能分别对个人技能及军队属性进行加点,其中个人节能消耗技能点,而军队属性加点消耗金币。后续将开发宫殿和主角卧式场景,通过对话进行剧情。

Unity经典游戏教程之:弓之骑士 Unity经典游戏教程之:弓之骑士

在Battle场景中,玩家可以操控主角射箭进行攻击,箭沿着鼠标方向射出,并可以通过鼠标蓄力使箭射出的速度更快。同时金币会自动增长,玩家可以花费一定的金币出对应的兵种进行对战,敌方也将发兵,玩家带领友方小兵攻破敌方城堡即为获胜。

Unity经典游戏教程之:弓之骑士

在Boss战场景中,玩家需要与Boss进行对抗,Boss会不断发射弹幕,玩家需要躲避,Boss平时为无敌状态,在特定时刻Boss会在身上生成弱点,玩家击中弱点即可击杀Boss。

Unity经典游戏教程之:弓之骑士

三 主角控制

1 玩家左右移动

首先为玩家添加Box Collider2D碰撞器和RigidBody刚体,使其能进行物理运动。

玩家通过按键输入,系统接收信号并生成为Horizontal保留在系数h中,我们将主角水平方向上的速度设置为h*maxSpeed,即可使角色左右移动

float h = Input.GetAxis("Horizontal");


        Vector2 vec = rb.velocity;
        rb.velocity = new Vector2(h * maxSpeed, vec.y);

        this.transform.Find("PlayerBody").transform.localScale = new Vector3(Mathf.Sign(h) * 4, 4, 4);

2 玩家跳跃

添加一个bool变量Jump来表示角色当前能否进行跳跃,同时在Update中从玩家位置往下射长度为0.7的线,看是否能碰到Ground层,即是否碰到了地面,如果碰到了店面则表示此时角色着地,可以进行跳跃,当按下了跳跃键且此时射线碰到了地面时,则将跳跃开关打开,在FixedUpdate中,当检测到跳跃开关为打开状态时,则为角色添加一个向上的力,并将跳跃开关关闭。

// 从起点向方向点发射一条特定距离的射线,看是否碰到了层“Ground”,返回bool值
        RaycastHit2D hit = Physics2D.Raycast(this.transform.position, Vector2.down, 0.7f, 1 << LayerMask.NameToLayer("Ground"));

        Debug.DrawRay(this.transform.position, Vector2.down * 0.7f); // 测试划线

        Vector2 vec = rb.velocity;

        if (hit && Mathf.Abs(vec.y) < 0.01f)
        {
            am.SetBool("IsJump", false);
        }

        if (Input.GetButtonDown("Jump") && hit) // 按下了跳跃键并且此时是着地的
        {
            jump = true; // 更改为可跳状态(要在FixedUpdate中进行物理跳跃)
        }


if (jump)
        {
            Vector2 vel = rb.velocity;
            vel.y = jumpForce;
            rb.velocity = vel;
            //rb.AddForce(Vector2.up * jumpForce);

            jump = false; // 跳起后将是否可跳起状态重置为否

            am.SetBool("IsJump", true);

        }

3 玩家动画

玩家动画控制器Animator如下图所示,不进行任何操作时,为Idle状态,检测角色水平方向上的速度并传递给DirX来控制角色进入Run的状态,检测角色垂直方向上的速度并传递给DirY来控制角色进入Jump的状态,Jump结束后当垂直方向上的速度小于等于0.01时进入Fall的状态。其中,由于跳跃的至高点和着地时的速度状态一模一样,所以需要添加一个bool类型的Paramater来判断此时角色是否跳跃在空中,如果是则进入Fall,避免其在空中进入Idle的状态。

Unity经典游戏教程之:弓之骑士

4 蓄力

由于实测Input.GetMouseDown(0)的使用似乎有点不稳定,采用了OnMouseDown的方法。在空间中创建了一个Mouse,为其添加Rigidbody刚体,并使坐标跟随鼠标。

// 跟随鼠标
        Vector3 mouseWorldPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition); // 屏幕坐标向世界坐标转换
        mouseWorldPoint.z = 0;
        this.gameObject.transform.position = mouseWorldPoint;

这样鼠标必定会点击在Mouse上,在Mouse的脚本上添加OnMouseDown和OnMouseUp的方法,当按下鼠标时,记录当前时间,松开时再次记录时间,将两者的差值除以最大蓄力时间并将该系数限制在0到1之间,此时得到了最后的蓄力系数,将系数传递给能量条以控制能量条的显示,同时将其传递给PlayerControl脚本乘以施加的最大的力将箭射出,并在在方法执行两秒后再次生成箭。

public void OnMouseDown()
    {
        isHasArrow = bow.GetComponent<BowFollow>().isHasArrow; // 继承弓上的“是否生成箭状态”
        

        if (isHasArrow)
        {
            bow.GetComponent<BowFollow>().isPreparing = true;
            timeBefore = Time.time;
        }
    }

    public void OnMouseUp()
    {
        if (isHasArrow)
        {
            timeCurrent = Time.time;
            //OnShoot();
            //if (bow.transform.childCount == 1)
            //    return;
            bow.transform.GetComponent<BowFollow>().OnBowShoot(timeCurrent - timeBefore); // 控制弓,射出箭(传入蓄力时间)

            bow.transform.GetComponent<BowFollow>().OnSetPower(timeCurrent - timeBefore); // 蓄力条

            isHasArrow = false;
            bow.GetComponent<BowFollow>().isPreparing = false;
        }

    }

5 箭的转向

实时监测箭的速度,将垂直方向上的速度除以水平方向上的速度得到其斜率,利用Atan的数学方法算出此时的弧度,并用Rad2Deg将其转换为角度传递给箭的Rotation以控制其旋转。

if (isShoot) // 射出后跟随速度旋转
        {

            Vector2 vir = this.gameObject.GetComponent<Rigidbody2D>().velocity; // 当前速度方向
            float deg = Mathf.Rad2Deg * Mathf.Atan2(vir.y, vir.x); // 角度
            this.transform.rotation = Quaternion.Euler(0, 0, deg + 225f);

        }

四 数据传递

在所有的场景中加入一个Data对象,插入名为Data的脚本,将整个Data对象设置成预制体,并将其设置为DontDestroy,在Main Camera上插入Loader脚本以保证所有场景有且仅有一个Data。

将所有的相关数据保存在Data脚本中,并将Data脚本做成单件模式,这样当前场景的所有对象都能从Data中调用数据。

public static Data instance; // 单件模式

    public int checkPoint = 0; // 当前关卡

    #region 玩家经验等级
    public int curExp = 0;
    public int extreExp = 0;
    public int curLevel = 0;
    public int level_1 = 100;
    public int level_2 = 200;
    public int level_3 = 400;
    public int level_4 = 600;
    public int level_5 = 10000;
    #endregion

    #region 玩家移动
    public float moveSpeed = 2.5f; // 移动速度
    public float jumpForce = 6f; // 跳跃所施加的力
    #endregion

    #region 玩家攻击
    public float maxAccumulatedTime = 2f; // 最大蓄力时间
    public float loadingSpeed = 2f;
    public float damage = 10f; // 伤害
    public float baseForce = 400; // 基础受力
    public float additionalForce = 200; // 额外受力
    #endregion

    #region 金币
    public int curGold = 0; // 当前金币
    public int addGold = 2; // 每秒增加的金币
    public int castleGold_1 = 500; // 通关第一关城堡所获得的金币
    #endregion

    #region 玩家技能
    public int curSkill = 0;

    // 被动技能
    public bool quickShoot = false; // 速射技能(减少蓄力时间):Bow
    public float quickShootTime = 1.5f;
    public bool quickLoading = false; // 快速装填技能:Bow
    public float quickLoadingSpeed = 1.5f;
    public bool forceUp = false; // 鹰眼技能:Bow
    public float forceUpForce = 500f;
    public bool damageUp = false; // 增伤技能:Arrow
    public float damageUpDamage = 15f;
    public bool speedUp = false; // 加速技能:PlayerControl
    public float speedUpSpeed = 3.5f;
    public bool expUp = false; // 获得经验值增加技能:PlayerControl
    public int expUpExp = 1;

    // 火箭相关
    public float fireDamage = 10f;
    public int fireNum = 3;
    public float fireCD = 5;

    public bool fireUp = false;
    public bool fireDamageUp = false; // 增加伤害技能
    public float fireDamageUpDamage = 15f;
    public bool fireNumUp = false; // 增加数量技能
    public int fireNumUpNum = 5;

    // 冰箭相关
    public float iceDamage = 5f;
    public int iceNum = 3;
    public float iceCD = 5;

    public bool iceUp = false;
    public bool iceNumUp = false; // 增加数量技能
    public int iceNumUpNum = 5;
    public bool icePush = false;

    // 三重箭相关
    public bool threeArrows = false;

    // 闪现相关
    public bool blink = false;
    public float blinkCD = 5f;
    #endregion

    #region 军队加点
    public bool swordmanHp_1 = false;
    public int swordmanHpGold_1 = 20;
    public bool swordmanHp_2 = false;
    public int swordmanHpGold_2 = 30;
    public bool swordmanHp_3 = false;
    public int swordmanHpGold_3 = 40;

    public bool swordmanDamage_1 = false;
    public int swordmanDamageGold_1 = 20;
    public bool swordmanDamage_2 = false;
    public int swordmanDamageGold_2 = 30;
    public bool swordmanDamage_3 = false;
    public int swordmanDamageGold_3 = 40;

    public bool rangerHp_1 = false;
    public int rangerHpGold_1 = 20;
    public bool rangerHp_2 = false;
    public int rangerHpGold_2 = 30;
    public bool rangerHp_3 = false;
    public int rangerHpGold_3 = 40;

    public bool rangerDamage_1 = false;
    public int rangerDamageGold_1 = 20;
    public bool rangerDamage_2 = false;
    public int rangerDamageGold_2 = 30;
    public bool rangerDamage_3 = false;
    public int rangerDamageGold_3 = 40;

    public bool wizardHp_1 = false;
    public int wizardHpGold_1 = 20;
    public bool wizardHp_2 = false;
    public int wizardHpGold_2 = 30;
    public bool wizardHp_3 = false;
    public int wizardHpGold_3 = 40;

    public bool wizardDamage_1 = false;
    public int wizardDamageGold_1 = 20;
    public bool wizardDamage_2 = false;
    public int wizardDamageGold_2 = 30;
    public bool wizardDamage_3 = false;
    public int wizardDamageGold_3 = 40;

    public bool wizardMagicDamage_1 = false;
    public int wizardMagicDamageGold_1 = 20;
    public bool wizardMagicDamage_2 = false;
    public int wizardMagicDamageGold_2 = 30;
    public bool wizardMagicDamage_3 = false;
    public int wizardMagicDamageGold_3 = 40;
    #endregion

    #region 友方战士
    public float swordmanTeammate_MaxHp = 50f;
    public float swordmanTeammate_damage = 10f;
    public int swordmanGold = 10;
    #endregion

    #region 友方射手
    public float rangerTeammate_MaxHp = 20f;
    public float rangerTeammate_damage = 10f;
    public int rangerGold = 10;
    #endregion

    #region 友方法师
    public float wizardTeammate_MaxHp = 20f;
    public float wizardTeammate_damage = 10f;
    public float wizardTeammate_magicDamage = 5f;
    public int wizardGold = 20;
    #endregion

    #region 敌方战士
    public float swordmanEnemy_MaxHp = 50f;
    public float swordmanEnemy_damage = 10f;
    public int swordmanExp = 10;
    #endregion

    #region 敌方射手
    public float rangerEnemy_MaxHp = 20f;
    public float rangerEnemy_damage = 10f;
    public int rangerExp = 10;
    #endregion

    #region 敌方法师
    public float wizardEnemy_MaxHp = 20f;
    public float wizardEnemy_damage = 10f;
    public float wizardEnemy_magicDamage = 5f;
    public int wizardExp = 20;
    #endregion

在所有需要调用数据的脚本文件的Start方法中调用数据,如在PlayerControl的脚本的Start方法中将Data的伤害信息赋给PlayerControl的伤害信息,如果中间产生了改变,则在PlayerControl的OnDestroy方法中将伤害信息重新赋值回Data,这样整个的伤害信息就能进行传递。

        #region 数据载入
        curLevel = Data.instance.curLevel;
        curExp = Data.instance.curExp;
        extreExp = Data.instance.extreExp;
        switch (curLevel)
        {
            case 0:
                curMaxExp = Data.instance.level_1;
                break;
            case 1:
                curMaxExp = Data.instance.level_2;
                break;
            case 2:
                curMaxExp = Data.instance.level_3;
                break;
            case 3:
                curMaxExp = Data.instance.level_4;
                break;
            case 4:
                curMaxExp = Data.instance.level_5;
                break;
        }
        curGold = Data.instance.curGold;
        addGold = Data.instance.addGold;
        curSkill = Data.instance.curSkill;

        maxSpeed = Data.instance.moveSpeed;
        jumpForce = Data.instance.jumpForce;


        #endregion

五 AI

本章以法师AI(Wizard)为例,介绍AI控制

1 动画控制器

Wizard的动画控制器如下所示。

正常情况下在创建时法师直接进入Walk状态并在Update中利用射线检测前方打击范围内是否存在敌人,一旦存在敌人立马进入Attack状态,攻击完后将IsAttacked设为true并进入Idle作为攻击后摇。Idle末尾进行判断,如果已经攻击过,则释放魔法,进入Magic动画,如果还没有攻击过,则进入Attack动画,如果没有检测到敌人则进入Walk状态。这样如果前方一直有敌人,则动画会进入攻击-等待-魔法-等待-攻击的循环直到前方敌人消失。

Unity经典游戏教程之:弓之骑士

2 动画事件

主要的动画事件包括三个,第一个是在Attack的末尾加入OnAttack方法,生成普通攻击弹幕;第二个是Idle的末尾加入OnJudeg方法,判断此时该进入Attack、Magic和Walk的哪一个;第三个是Magic的末尾加入OnMagic方法,生成魔法特效。

3 数据来源

如同GameControler,所有AI的脚本在一开始的Start方法中,从Data引入数据,包括AI的最大血量及伤害等等。因为未来的加点可能会影响AI的数据所以不能在代码中给其赋值。

        maxHp = Data.instance.wizardEnemy_MaxHp;
        attackDamage = Data.instance.wizardEnemy_damage;
        magicDamage = Data.instance.wizardEnemy_magicDamage;

4 近战伤害

由于近战伤害是通过RaycastHit2D来判断前方是否有敌人,所以hit时可以调用敌方对象上所加的脚本上的GetHurt方法来对对象进行扣血操作。

public void OnAttack() // 在Attack动画事件中调用此方法
    {
        RaycastHit2D hit = Physics2D.Linecast(this.transform.position, (Vector2)hitPoint.transform.position, 1 << LayerMask.NameToLayer("Teammate"));
        if (hit)
        {
            if (hit.transform.GetComponent<TeammateHurt>() != null)
            {
                hit.transform.GetComponent<TeammateHurt>().GetHurt(damage);
            }
            else
            {
                hit.transform.GetComponent<TeammateCreate>().GetHurt(damage);
            }


        }

        this.gameObject.GetComponent<Animator>().SetTrigger("Idle");
    }

5 射箭抛物线

弓箭手在进行攻击时,首先会侦测射程范围内有多少敌人,利用OverlapCircleAll方法将所有可攻击的对象放在一个数组中,并挨个检测自身与敌方的距离是不是最远的,从而选出距离最远的打击目标。

选出打击目标后,利用抛物线算出在一定速度下箭应具有的射出角度,并将箭射出。

public void OnAttack() // 在Attack动画事件中调用此方法 
    {
        // 侦测最远打击对象
        float maxLength = 0;
        GameObject hitTarget;

        Collider2D[] enemies = Physics2D.OverlapCircleAll(this.transform.position, radius, 1 << LayerMask.NameToLayer("Teammate"));

        foreach (Collider2D e in enemies)
        {
            Vector3 interval = e.transform.position - this.transform.position; // 间隔
            if (interval.magnitude >= maxLength)
            {
                maxLength = interval.magnitude;
                hitTarget = e.gameObject;
            }
        }

        // 计算力
        float rad = (Mathf.Asin(maxLength * 10f / shootSpeed / shootSpeed)) / 2;
        Vector2 vec = new Vector2(-shootSpeed * Mathf.Cos(rad), shootSpeed * Mathf.Sin(rad));
        //?


        // 射箭
        GameObject arrow = Instantiate(enemyBarrage); // 生成箭

        arrow.transform.GetComponent<ArrowEnemy>().damage = damage; // 将个人伤害赋到箭上(后期可以更改弓兵伤害)
        arrow.transform.position = this.transform.position;
        arrow.GetComponent<Rigidbody2D>().velocity = vec;

        // 进入Idle状态
        am.SetTrigger("Idle");
    }

6 魔法及动画

法师的魔法技能,首先通过RaycastHit2D方法判断打击范围敌人的位置,并在其位置上空生成魔法云,魔法云通过动画控制其collider从下往上靠近敌人,对一定范围内的所有敌人产生伤害。

public void OnMagic() // 在Magic动画事件中调用此方法
    {
        am.SetBool("IsAttacked", false);
        isHasAttacked = false;

        RaycastHit2D hit = Physics2D.Linecast(this.transform.position, (Vector2)hitPoint.transform.position, 1 << LayerMask.NameToLayer("Enemy"));

        GameObject magic = Instantiate(wizardMagic);
        magic.GetComponent<WizardMagic>().state = state;
        magic.GetComponent<WizardMagic>().damage = magicDamage;

        if (hit)
        {
            magic.transform.position = new Vector3(hit.collider.transform.position.x, -2.37f, 0);
        }
        
    }

Unity经典游戏教程之:弓之骑士

7 状态枚举

public enum State
{
    // 敌对关系
    Teammate,
    Enemy,
}

public enum Category
{
    // 兵种类别
    Swordman,
    Ranger,
    Wizard,

    // 
    Castle,

    // 弹幕类别
    Arrwo,
    Barrage,
}

public enum Skill
{
    blink,
}

public enum Prop
{
    normal,
    fire,
    ice,
}

六 UI

1 血条和经验条

城堡的血条利用缩放来控制,将当前血量和最大血量的比值作为血条水平方向上的缩放比例。

Unity经典游戏教程之:弓之骑士

2 属性

利用UI的Text功能实现,在载入时读取Data中的相关数据并赋给Text的值。

Unity经典游戏教程之:弓之骑士

3 出兵CD

利用UI中的Image组件实现,将子节点用于遮挡的半透明图片类型设置为Filled,此时将1-经过时间/技能CD作为系数传递给Image,就能实现技能的CD条。

Unity经典游戏教程之:弓之骑士

七 未来改进

1 改bug

2 技能完善

3 界面UI完善

4 代码优化

5 攻击间隔改进

6 补充剧情及触发动画

7 弱点优化

8 镜头震动

9 粒子系统

10 敌方出动的AI改进

11 主角移动范围

12 代码快速调用

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Karen110 Karen110
3年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Docker 部署SpringBoot项目不香吗?
  公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。  !(http://dingyue.ws.126.net/2020/0920/b00fbfc7j00qgy5xy002kd200qo00hsg00it00cj.jpg)  2
Stella981 Stella981
3年前
Fire Balls 11——修正游戏的BUG
版权申明:本文原创首发于以下网站:1.博客园『优梦创客』的空间:https://www.cnblogs.com/raymondking123(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.cnblogs.com%2Fraymondking123%
Wesley13 Wesley13
3年前
Unity经典游戏教程之:是男人就下100层
版权声明:本文原创发布于博客园"优梦创客"的博客空间(网址:http://www.cnblogs.com/raymondking123/)以及微信公众号"优梦创客"(微信号:unitymaker)您可以自由转载,但必须加入完整的版权声明!是男人就下一百层一、游戏介绍是男人就
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这