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

版权声明:

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

一 原始游戏

9pmTgO.png

原始游戏玩法:

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

9pmLbd.png

参考游戏玩法:

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

二 改进的个人项目玩法

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

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

9pmXVA.png 9pmv5t.png

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

9pmzPP.png

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

9pnp28.png

三 主角控制

1 玩家左右移动

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

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

1float h = Input.GetAxis("Horizontal"); 2 3 4 Vector2 vec = rb.velocity; 5 rb.velocity = new Vector2(h * maxSpeed, vec.y); 6 7 this.transform.Find("PlayerBody").transform.localScale = new Vector3(Mathf.Sign(h) * 4, 4, 4);

2 玩家跳跃

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

1// 从起点向方向点发射一条特定距离的射线,看是否碰到了层“Ground”,返回bool值 2 RaycastHit2D hit = Physics2D.Raycast(this.transform.position, Vector2.down, 0.7f, 1 << LayerMask.NameToLayer("Ground")); 3 4 Debug.DrawRay(this.transform.position, Vector2.down * 0.7f); // 测试划线 5 6 Vector2 vec = rb.velocity; 7 8 if (hit && Mathf.Abs(vec.y) < 0.01f) 9 { 10 am.SetBool("IsJump", false); 11 } 12 13 if (Input.GetButtonDown("Jump") && hit) // 按下了跳跃键并且此时是着地的 14 { 15 jump = true; // 更改为可跳状态(要在FixedUpdate中进行物理跳跃) 16 } 17 18 19if (jump) 20 { 21 Vector2 vel = rb.velocity; 22 vel.y = jumpForce; 23 rb.velocity = vel; 24 //rb.AddForce(Vector2.up * jumpForce); 25 26 jump = false; // 跳起后将是否可跳起状态重置为否 27 28 am.SetBool("IsJump", true); 29 30 }

3 玩家动画

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

9pnV5q.png

4 蓄力

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

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

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

1public void OnMouseDown() 2 { 3 isHasArrow = bow.GetComponent<BowFollow>().isHasArrow; // 继承弓上的“是否生成箭状态” 4 5 6 if (isHasArrow) 7 { 8 bow.GetComponent<BowFollow>().isPreparing = true; 9 timeBefore = Time.time; 10 } 11 } 12 13 public void OnMouseUp() 14 { 15 if (isHasArrow) 16 { 17 timeCurrent = Time.time; 18 //OnShoot(); 19 //if (bow.transform.childCount == 1) 20 // return; 21 bow.transform.GetComponent<BowFollow>().OnBowShoot(timeCurrent - timeBefore); // 控制弓,射出箭(传入蓄力时间) 22 23 bow.transform.GetComponent<BowFollow>().OnSetPower(timeCurrent - timeBefore); // 蓄力条 24 25 isHasArrow = false; 26 bow.GetComponent<BowFollow>().isPreparing = false; 27 } 28 29 }

5 箭的转向

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

1if (isShoot) // 射出后跟随速度旋转 2 { 3 4 Vector2 vir = this.gameObject.GetComponent<Rigidbody2D>().velocity; // 当前速度方向 5 float deg = Mathf.Rad2Deg * Mathf.Atan2(vir.y, vir.x); // 角度 6 this.transform.rotation = Quaternion.Euler(0, 0, deg + 225f); 7 8 }

四 数据传递

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

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

1public static Data instance; // 单件模式 2 3 public int checkPoint = 0; // 当前关卡 4 5 #region 玩家经验等级 6 public int curExp = 0; 7 public int extreExp = 0; 8 public int curLevel = 0; 9 public int level_1 = 100; 10 public int level_2 = 200; 11 public int level_3 = 400; 12 public int level_4 = 600; 13 public int level_5 = 10000; 14 #endregion 15 16 #region 玩家移动 17 public float moveSpeed = 2.5f; // 移动速度 18 public float jumpForce = 6f; // 跳跃所施加的力 19 #endregion 20 21 #region 玩家攻击 22 public float maxAccumulatedTime = 2f; // 最大蓄力时间 23 public float loadingSpeed = 2f; 24 public float damage = 10f; // 伤害 25 public float baseForce = 400; // 基础受力 26 public float additionalForce = 200; // 额外受力 27 #endregion 28 29 #region 金币 30 public int curGold = 0; // 当前金币 31 public int addGold = 2; // 每秒增加的金币 32 public int castleGold_1 = 500; // 通关第一关城堡所获得的金币 33 #endregion 34 35 #region 玩家技能 36 public int curSkill = 0; 37 38 // 被动技能 39 public bool quickShoot = false; // 速射技能(减少蓄力时间):Bow 40 public float quickShootTime = 1.5f; 41 public bool quickLoading = false; // 快速装填技能:Bow 42 public float quickLoadingSpeed = 1.5f; 43 public bool forceUp = false; // 鹰眼技能:Bow 44 public float forceUpForce = 500f; 45 public bool damageUp = false; // 增伤技能:Arrow 46 public float damageUpDamage = 15f; 47 public bool speedUp = false; // 加速技能:PlayerControl 48 public float speedUpSpeed = 3.5f; 49 public bool expUp = false; // 获得经验值增加技能:PlayerControl 50 public int expUpExp = 1; 51 52 // 火箭相关 53 public float fireDamage = 10f; 54 public int fireNum = 3; 55 public float fireCD = 5; 56 57 public bool fireUp = false; 58 public bool fireDamageUp = false; // 增加伤害技能 59 public float fireDamageUpDamage = 15f; 60 public bool fireNumUp = false; // 增加数量技能 61 public int fireNumUpNum = 5; 62 63 // 冰箭相关 64 public float iceDamage = 5f; 65 public int iceNum = 3; 66 public float iceCD = 5; 67 68 public bool iceUp = false; 69 public bool iceNumUp = false; // 增加数量技能 70 public int iceNumUpNum = 5; 71 public bool icePush = false; 72 73 // 三重箭相关 74 public bool threeArrows = false; 75 76 // 闪现相关 77 public bool blink = false; 78 public float blinkCD = 5f; 79 #endregion 80 81 #region 军队加点 82 public bool swordmanHp_1 = false; 83 public int swordmanHpGold_1 = 20; 84 public bool swordmanHp_2 = false; 85 public int swordmanHpGold_2 = 30; 86 public bool swordmanHp_3 = false; 87 public int swordmanHpGold_3 = 40; 88 89 public bool swordmanDamage_1 = false; 90 public int swordmanDamageGold_1 = 20; 91 public bool swordmanDamage_2 = false; 92 public int swordmanDamageGold_2 = 30; 93 public bool swordmanDamage_3 = false; 94 public int swordmanDamageGold_3 = 40; 95 96 public bool rangerHp_1 = false; 97 public int rangerHpGold_1 = 20; 98 public bool rangerHp_2 = false; 99 public int rangerHpGold_2 = 30; 100 public bool rangerHp_3 = false; 101 public int rangerHpGold_3 = 40; 102 103 public bool rangerDamage_1 = false; 104 public int rangerDamageGold_1 = 20; 105 public bool rangerDamage_2 = false; 106 public int rangerDamageGold_2 = 30; 107 public bool rangerDamage_3 = false; 108 public int rangerDamageGold_3 = 40; 109 110 public bool wizardHp_1 = false; 111 public int wizardHpGold_1 = 20; 112 public bool wizardHp_2 = false; 113 public int wizardHpGold_2 = 30; 114 public bool wizardHp_3 = false; 115 public int wizardHpGold_3 = 40; 116 117 public bool wizardDamage_1 = false; 118 public int wizardDamageGold_1 = 20; 119 public bool wizardDamage_2 = false; 120 public int wizardDamageGold_2 = 30; 121 public bool wizardDamage_3 = false; 122 public int wizardDamageGold_3 = 40; 123 124 public bool wizardMagicDamage_1 = false; 125 public int wizardMagicDamageGold_1 = 20; 126 public bool wizardMagicDamage_2 = false; 127 public int wizardMagicDamageGold_2 = 30; 128 public bool wizardMagicDamage_3 = false; 129 public int wizardMagicDamageGold_3 = 40; 130 #endregion 131 132 #region 友方战士 133 public float swordmanTeammate_MaxHp = 50f; 134 public float swordmanTeammate_damage = 10f; 135 public int swordmanGold = 10; 136 #endregion 137 138 #region 友方射手 139 public float rangerTeammate_MaxHp = 20f; 140 public float rangerTeammate_damage = 10f; 141 public int rangerGold = 10; 142 #endregion 143 144 #region 友方法师 145 public float wizardTeammate_MaxHp = 20f; 146 public float wizardTeammate_damage = 10f; 147 public float wizardTeammate_magicDamage = 5f; 148 public int wizardGold = 20; 149 #endregion 150 151 #region 敌方战士 152 public float swordmanEnemy_MaxHp = 50f; 153 public float swordmanEnemy_damage = 10f; 154 public int swordmanExp = 10; 155 #endregion 156 157 #region 敌方射手 158 public float rangerEnemy_MaxHp = 20f; 159 public float rangerEnemy_damage = 10f; 160 public int rangerExp = 10; 161 #endregion 162 163 #region 敌方法师 164 public float wizardEnemy_MaxHp = 20f; 165 public float wizardEnemy_damage = 10f; 166 public float wizardEnemy_magicDamage = 5f; 167 public int wizardExp = 20; 168 #endregion

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

1 #region 数据载入 2 curLevel = Data.instance.curLevel; 3 curExp = Data.instance.curExp; 4 extreExp = Data.instance.extreExp; 5 switch (curLevel) 6 { 7 case 0: 8 curMaxExp = Data.instance.level_1; 9 break; 10 case 1: 11 curMaxExp = Data.instance.level_2; 12 break; 13 case 2: 14 curMaxExp = Data.instance.level_3; 15 break; 16 case 3: 17 curMaxExp = Data.instance.level_4; 18 break; 19 case 4: 20 curMaxExp = Data.instance.level_5; 21 break; 22 } 23 curGold = Data.instance.curGold; 24 addGold = Data.instance.addGold; 25 curSkill = Data.instance.curSkill; 26 27 maxSpeed = Data.instance.moveSpeed; 28 jumpForce = Data.instance.jumpForce; 29 30 31 #endregion 32

五 AI

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

1 动画控制器

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

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

9pnHJ0.png

2 动画事件

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

3 数据来源

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

1 maxHp = Data.instance.wizardEnemy_MaxHp; 2 attackDamage = Data.instance.wizardEnemy_damage; 3 magicDamage = Data.instance.wizardEnemy_magicDamage;

4 近战伤害

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

1public void OnAttack() // 在Attack动画事件中调用此方法 2 { 3 RaycastHit2D hit = Physics2D.Linecast(this.transform.position, (Vector2)hitPoint.transform.position, 1 << LayerMask.NameToLayer("Teammate")); 4 if (hit) 5 { 6 if (hit.transform.GetComponent<TeammateHurt>() != null) 7 { 8 hit.transform.GetComponent<TeammateHurt>().GetHurt(damage); 9 } 10 else 11 { 12 hit.transform.GetComponent<TeammateCreate>().GetHurt(damage); 13 } 14 15 16 } 17 18 this.gameObject.GetComponent<Animator>().SetTrigger("Idle"); 19 }

5 射箭抛物线

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

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

1public void OnAttack() // 在Attack动画事件中调用此方法 2 { 3 // 侦测最远打击对象 4 float maxLength = 0; 5 GameObject hitTarget; 6 7 Collider2D[] enemies = Physics2D.OverlapCircleAll(this.transform.position, radius, 1 << LayerMask.NameToLayer("Teammate")); 8 9 foreach (Collider2D e in enemies) 10 { 11 Vector3 interval = e.transform.position - this.transform.position; // 间隔 12 if (interval.magnitude >= maxLength) 13 { 14 maxLength = interval.magnitude; 15 hitTarget = e.gameObject; 16 } 17 } 18 19 // 计算力 20 float rad = (Mathf.Asin(maxLength * 10f / shootSpeed / shootSpeed)) / 2; 21 Vector2 vec = new Vector2(-shootSpeed * Mathf.Cos(rad), shootSpeed * Mathf.Sin(rad)); 22 //? 23 24 25 // 射箭 26 GameObject arrow = Instantiate(enemyBarrage); // 生成箭 27 28 arrow.transform.GetComponent<ArrowEnemy>().damage = damage; // 将个人伤害赋到箭上(后期可以更改弓兵伤害) 29 arrow.transform.position = this.transform.position; 30 arrow.GetComponent<Rigidbody2D>().velocity = vec; 31 32 // 进入Idle状态 33 am.SetTrigger("Idle"); 34 }

6 魔法及动画

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

1public void OnMagic() // 在Magic动画事件中调用此方法 2 { 3 am.SetBool("IsAttacked", false); 4 isHasAttacked = false; 5 6 RaycastHit2D hit = Physics2D.Linecast(this.transform.position, (Vector2)hitPoint.transform.position, 1 << LayerMask.NameToLayer("Enemy")); 7 8 GameObject magic = Instantiate(wizardMagic); 9 magic.GetComponent<WizardMagic>().state = state; 10 magic.GetComponent<WizardMagic>().damage = magicDamage; 11 12 if (hit) 13 { 14 magic.transform.position = new Vector3(hit.collider.transform.position.x, -2.37f, 0); 15 } 16 17 }

9C4ySe.png

7 状态枚举

1public enum State 2{ 3 // 敌对关系 4 Teammate, 5 Enemy, 6} 7 8public enum Category 9{ 10 // 兵种类别 11 Swordman, 12 Ranger, 13 Wizard, 14 15 // 16 Castle, 17 18 // 弹幕类别 19 Arrwo, 20 Barrage, 21} 22 23public enum Skill 24{ 25 blink, 26} 27 28public enum Prop 29{ 30 normal, 31 fire, 32 ice, 33}

六 UI

1 血条和经验条

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

9C5acQ.png

2 属性

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

9CIGr9.png

3 出兵CD

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

9CITqs.png

七 未来改进

1 改bug

2 技能完善

3 界面UI完善

4 代码优化

5 攻击间隔改进

6 补充剧情及触发动画

7 弱点优化

8 镜头震动

9 粒子系统

10 敌方出动的AI改进

11 主角移动范围

12 代码快速调用

点赞
收藏

评论区

加载中...

相关推荐

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

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

一篇文章带你了解JavaScript日期

日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用