版权声明:
- 本文原创发布于博客园"优梦创客"的博客空间(网址:http://www.cnblogs.com/raymondking123/)以及微信公众号“优梦创客”(微信号:unitymaker)
- 您可以自由转载,但必须加入完整的版权声明!
一 原始游戏
原始游戏玩法:
游戏名:弓箭手。玩家控制拿着弓箭的弓箭手,玩家AD键控制弓箭手左右移动,鼠标进行射击,同时鼠标可长按进行蓄力,使得弓箭射出的速度更快,箭能射的更远。两边自动生成战士AI,左边为友方AI,右边为敌方AI,相遇时会作战,玩家带领友方AI进攻至最右边方可胜利,同样的,敌方AI进攻至最左边时则游戏失败。
参考游戏玩法:
游戏名:战争进化史。玩家点击右上角按钮,花费金币购买兵种进行战斗,攻破敌方城堡即为胜利。
二 改进的个人项目玩法
本游戏(暂时)有三个场景,分别为Castle场景、Battle场景和Boss战场景。
在Castle场景中,玩家操控主角在王国中行走,可以通过房子的们进入房子,当前开发了训练室和练兵场两块场地,进入后能分别对个人技能及军队属性进行加点,其中个人节能消耗技能点,而军队属性加点消耗金币。后续将开发宫殿和主角卧式场景,通过对话进行剧情。
在Battle场景中,玩家可以操控主角射箭进行攻击,箭沿着鼠标方向射出,并可以通过鼠标蓄力使箭射出的速度更快。同时金币会自动增长,玩家可以花费一定的金币出对应的兵种进行对战,敌方也将发兵,玩家带领友方小兵攻破敌方城堡即为获胜。

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

三 主角控制
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的状态。
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状态。这样如果前方一直有敌人,则动画会进入攻击-等待-魔法-等待-攻击的循环直到前方敌人消失。
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);
        }
        
    }
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 血条和经验条
城堡的血条利用缩放来控制,将当前血量和最大血量的比值作为血条水平方向上的缩放比例。
2 属性
利用UI的Text功能实现,在载入时读取Data中的相关数据并赋给Text的值。
3 出兵CD
利用UI中的Image组件实现,将子节点用于遮挡的半透明图片类型设置为Filled,此时将1-经过时间/技能CD作为系数传递给Image,就能实现技能的CD条。
七 未来改进
1 改bug
2 技能完善
3 界面UI完善
4 代码优化
5 攻击间隔改进
6 补充剧情及触发动画
7 弱点优化
8 镜头震动
9 粒子系统
10 敌方出动的AI改进
11 主角移动范围
12 代码快速调用










 
  
  
  
 
 
  
 
 
 