Unity经典游戏教程之:弓之骑士
版权声明:
- 本文原创发布于博客园"优梦创客"的博客空间(网址:
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 代码快速调用
Unity经典游戏教程之:弓之骑士的更多相关文章
- Unity经典游戏教程之:雪人兄弟
版权声明: 本文原创发布于博客园"优梦创客"的博客空间(网址:http://www.cnblogs.com/raymondking123/)以及微信公众号"优梦创客&qu ...
- Unity经典游戏教程之:贪吃蛇
版权声明: 本文原创发布于博客园"优梦创客"的博客空间(网址:http://www.cnblogs.com/raymondking123/)以及微信公众号"优梦创客&qu ...
- Unity经典游戏教程之:是男人就下100层
版权声明: 本文原创发布于博客园"优梦创客"的博客空间(网址:http://www.cnblogs.com/raymondking123/)以及微信公众号"优梦创客&qu ...
- Unity经典游戏教程之:冒险岛
版权声明: 本文原创发布于博客园"优梦创客"的博客空间(网址:http://www.cnblogs.com/raymondking123/)以及微信公众号"优梦创客&qu ...
- Unity经典游戏教程之:合金弹头
版权声明: 本文原创发布于博客园"优梦创客"的博客空间(网址:http://www.cnblogs.com/raymondking123/)以及微信公众号"优梦创客&qu ...
- Unity经典游戏编程之:球球大作战
版权声明: 本文原创发布于博客园"优梦创客"的博客空间(网址:http://www.cnblogs.com/raymondking123/)以及微信公众号"优梦创客&qu ...
- C#开发Unity游戏教程之Unity中方法的参数
C#开发Unity游戏教程之Unity中方法的参数 Unity的方法的参数 出现在脚本中的方法,无论是在定义的时候,还是使用的时候,后面都跟着一对括号“( )”,有意义吗?看起来最多也就是起个快速识别 ...
- C#开发Unity游戏教程之Scene视图与脚本的使用
C#开发Unity游戏教程之Scene视图与脚本的使用 Unity中Scene视图的快捷操作 Scene视图是开发者开发游戏时,操作最频繁的视图.因为一旦一个游戏对象被添加到游戏的场景中,就需要首先使 ...
- Unity实战案例教程之:不免费的PacMan(初级→中级)
课程内容介绍: 本套课程适合以下人士: - 免费资料没教会你游戏开发的: - 学了Unity基础不知道怎么用在游戏项目里的: - 想快速开发一款好玩的游戏的: - 想学游戏不知道如何入门的: - 对游 ...
随机推荐
- DFS(一):深度优先搜索的基本思想
采用搜索算法解决问题时,需要构造一个表明状态特征和不同状态之间关系的数据结构,这种数据结构称为结点.不同的问题需要用不同的数据结构描述. 根据搜索问题所给定的条件,从一个结点出发,可以生成一个或多个新 ...
- php中\r \r\n \t的区别
\n 软回车: 在Windows 中表示换行且回到下一行的最开始位置.相当于Mac OS 里的 \r 的效果. 在Linux.unix 中只表示换行,但不会回到下一行的开始位置. ...
- redis整合springboot的helloworld
引入依赖 compile 'org.springframework.boot:spring-boot-starter-data-redis' 使用redis有两种方法 1.Jedis Jedis je ...
- JAVA增强for循环
作用:简化数组和集合的遍历 格式:for(元素数据类型 变量 :数组或者集合) 例子: Map map=new HashMap; for(Object obj :map.keySet()){ Obj ...
- POJ 2728:Desert King(最优比率生成树)
http://poj.org/problem?id=2728 题意:有n个点,有三个属性代表每个点在平面上的位置,和它的高度.点与点之间有一个花费:两点的高度差:还有一个长度:两点的距离.现在要让你在 ...
- 嵊州D1T3 睡美人航班
嵊州D1T3 睡美人航班 不知不觉中,我对她的爱意已经达到了 n. 是这样子的,第 1 分钟,我对她的爱意值是 (1, 1). 假如当第 x 分钟时我对她的爱意值是 (a, b),那么第 x + 1 ...
- C++学习书籍推荐《C++编程思想第二版第一卷》下载
百度云及其他网盘下载地址:点我 编辑推荐 “经典原版书库”是响应教育部提出的使用原版国外教材的号召,为国内高校的计算机教学度身订造的.<C++编程思想>(英文版第2版)是书库中的一本,在广 ...
- Java虚拟机详解(三)------垃圾回收
如果对C++这门语言熟悉的人,再来看Java,就会发现这两者对垃圾(内存)回收的策略有很大的不同. C++:垃圾回收很重要,我们必须要自己来回收!!! Java:垃圾回收很重要,我们必须交给系统来帮我 ...
- [POI2007]洪水pow 题解
[POI2007]洪水pow 时间限制: 5 Sec 内存限制: 128 MB 题目描述 AKD市处在一个四面环山的谷地里.最近一场大暴雨引发了洪水,AKD市全被水淹没了.Blue Mary,AKD ...
- android_alertDialog
主文件 package cn.com.sxp;import android.app.Activity;import android.app.AlertDialog;import android.con ...