版权声明:
- 本文原创发布于博客园"优梦创客"的博客空间(网址:
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 代码快速调用