一、关于场景搭建
Tilemap瓦片地图
1.先将一张贴图合集通过Sprite Mode从Single(单一图片)模式转变为Multiple(多张图片)模式以切割成为许多张小贴图入调色板当中,便可运用上方笔刷橡皮等制作一个简单的瓦片网格地图了。
2.在Hierarchy中创建2D project-Tilemap-rectangular创建网格地图
3.在windows中选择2D-tile palette创建一个New palette(新调色板)并将贴图合辑拖
二、关于玩家
1.玩家移动
2D游戏的玩家移动很简单,只需要获取一个水平轴的输入即可,不过既然是横板游戏,那玩家必然分为朝左移动和朝右移动,因此,单单地获取轴的输入时远远不够的,我们还需要给玩家一个翻转方法,防止出现玩家向后行走的情况
void Move() { float moveDir = Input.GetAxis("Horizontal"); Vector2 playerVel = new Vector2(moveDir * moveSpeed, rb.velocity.y); rb.velocity = playerVel; if(Mathf.Abs(rb.velocity.x)>0)//水平移动速度的绝对值大于零 { anim.SetBool("Run", true); if(rb.velocity.x > 0.0f)//速度大于零,向右移动 { if(!isFacingRight) { Flip();//翻转 } } else//速度小于零,向左移动 { if(isFacingRight) { Flip(); } } } else { anim.SetBool("Run", false); } }
当我们调用Flip方法,说明移动方向和面朝方向不一致,首先要做的就是让移动方向和面朝方向一致,更改是否朝右的bool值。而更改朝向的方法也很简单,我们可以通过玩家的Transform里面的Scale来进行缩放,而当我们x轴的缩放数值由1变为-1,便能实现玩家的竖直翻转
void Flip() { isFacingRight = !isFacingRight; Vector3 scale = transform.localScale; scale.x *= -1; transform.localScale = scale; }
2.玩家跳跃
1.地面检测
地面检测其实很简单,首先我们为玩家创建一个BoxCollider2D放在脚上用来检测是否和地面接触,这里记得要将其作为触发器,接着为我们的地面勾选上名为Ground的层,调用如下代码,便可以实现当脚触地时IsGround返回值为真了,当检测不到脚和地接触则IsGround返回值为假,这个方法需要在Update()里面实现,以便一直检测是否在地面
private BoxCollider2D myFeet; void CheckGround() { isGround = myFeet.IsTouchingLayers(LayerMask.GetMask("Ground")) }
2.实现跳跃和二段跳
跳跃的前提是玩家在地面上,不然玩家会直接升天
void Jump() { if(Input.GetButtonDown("Jump") && !Input.GetKey(KeyCode.S)) //这里是为了实现后续的功能才写下当不按下S时 { if (isGround) { anim.SetBool("Jump", true); Vector2 jumpVel = new Vector2(0.0f, jumpSpeed); rb.velocity = Vector2.up * jumpVel; //给玩家一个向上的速度 canDoubleJump = true; } else { if(canDoubleJump) { anim.SetBool("DoubleJump", true); Vector2 doublejumpVel = new Vector2(0.0f, doublejumpSpeed); rb.velocity = Vector2.up * doublejumpVel; //给玩家一个向上的二段跳速度 canDoubleJump = false; } } } }
3.运动动画
1.动画的转换
在Unity的动画控制器当中,大多都可以用Bool变量来改变动画,就如以上脚本的Run和Jump这需要我们在各个动画之间创建一条transition连线,同时创建一个bool变量,并更改其对应的发生方法,就例如当我想要从Idle动画转变为Run动画,我只需要在动画机连线内的Condition里将触发条件设为“Run true”即可实现 当我们在脚本中获取了动画机组件后,便可以在其中更改bool变量了
anim = GetComponent<Animator>(); //在Start方法中获取动画机组件 anim.SetBool("Run", true); //将动画机中Run这个bool变量设置为true
2.跳跃与下落动画的切换
我们都知道,在空中有两种情况,分别是向上跳和向下坠,但在上面的跳跃脚本中,我们只要不在地面就都是Jump的状态,这不是我们想要的,因此我们需要一个方法来区分我们在天上时究竟是上跳还是下坠
void SwitchAnimation() { if(anim.GetBool("Jump"))//如果当前是Jump的状态 { if(rb.velocity.y < 0.0f) { anim.SetBool("Jump",false); anim.SetBool("Fall",true); } } else if(isGround) { anim.SetBool("Fall", false); anim.SetBool("Idle",true ); } if (anim.GetBool("DoubleJump")) { if (rb.velocity.y < 0.0f) { anim.SetBool("DoubleJump", false); anim.SetBool("DoubleFall", true); } } else if (isGround) { anim.SetBool("DoubleFall", false); anim.SetBool("Idle", true); } }
4.玩家攀爬
1.攀爬检测
这个只需要用跟地面一样的方法即可,不一样的是我们需要给可攀爬的地方选上一个Ladder(梯子)的层
void CheckLadder() { isLadder = myFeet.IsTouchingLayers(LayerMask.GetMask("Ladder")); }
除此之外,我们不希望玩家从天上掉下来后可以掉到梯子上,因此我们还需要为玩家的空中状态进行一个检测
void CheckAirStatus() { isJumping = anim.GetBool("Jump"); isFalling = anim.GetBool("Fall"); isDoubleJumping = anim.GetBool("DoubleJump"); isDoubleFalling = anim.GetBool("DoubleFall"); isClimbing = anim.GetBool("Climbing"); }
2.实现攀爬
playerGravity = rb.gravityScale;//定义一个玩家重力使其等于刚体的重力 void Climb() { if(isLadder) { float MoveY = Input.GetAxis("Vertical"); if(MoveY > 0.5f || MoveY < -0.5f) { anim.SetBool("Climbing", true); rb.gravityScale = 0.0f;//将玩家重力改为零便可以实现在梯子上悬停 rb.velocity = new Vector2(rb.velocity.x , MoveY * climbSpeed); } else { if(isJumping || isFalling || isDoubleFalling || isDoubleJumping) //防止玩家以这几种情况接触到梯子也会悬停 { anim.SetBool("Climbing", false); } else { anim.SetBool("Climbing",false); rb.velocity = new Vector2(rb.velocity.x, 0.0f); } } } else//未与梯子接触 { anim.SetBool("Climbing", false); rb.gravityScale = playerGravity; } }
三、关于相机
1.相机跟随
在先前的玩家移动脚本中,我们把Move方法放在了Update当中,我们希望相机跟随实现在玩家移动之后,因此我们将其写在LateUpdate方法当中,其代表着在Update方法的后一帧执行
void LateUpdate() { if(target != null) { if(transform.position != target.position) { Vector3 targetPos = target.position; //定义目标位置 transform.position = Vector3.Lerp(transform.position, targetPos, smoothTime); //让摄像机位置等于玩家位置,并差值一个到达的平滑时间 } } }
2.限制相机移动范围
首先在地图边界的左下角和右上角分别找到一个点的坐标来划定地图的边界,并将摄像机的x,y值的最大最小范围用Clamp方法来限制它的极值
targetPos.x = Mathf.Clamp(targetPos.x, minPosition.x, maxPosition.x); targetPos.y = Mathf.Clamp(targetPos.y, minPosition.y, maxPosition.y)
public void SetCameraPosLimit(Vector2 minPos,Vector2 maxPos) { //此处定义的两个二维向量即为两个极限位置的坐标63. minPosition = minPos; maxPosition = maxPos; }
然后回到unity的界面在属性界面输入左下右上两个的边界点坐标即可
四、关于战斗
1.玩家攻击
在我们的攻击动画当中,有一段为挥刀的动画,我们希望在动画播放到挥刀的时候,出现一个可以检测刀体碰撞的碰撞体,因此我们需要用到一个携程来让刀碰撞体延后出现
anim = GameObject.FindGameObjectWithTag("Player").GetComponent<Animator>(); void Start() { anim = GameObject.FindGameObjectWithTag("Player").GetComponent<Animator>(); attackCollider = GetComponent<PolygonCollider2D>(); //在这里获取到挂载当前脚本的刀光所携带的碰撞体 attackCollider.enabled = false; } void Update() { Attack(); } void Attack() { if(Input.GetButtonDown("Attack")) //其中这个Attack我们只需要去到Project Setting中创建 { anim.SetTrigger("Attack"); StartCoroutine(startAttackBox()); } } IEnumerator startAttackBox() { yield return new WaitForSeconds(startTime); attackCollider.enabled = true; StartCoroutine(disableAttackBox()); } IEnumerator disableAttackBox() { yield return new WaitForSeconds(endTime); attackCollider.enabled = false; } //通过以上两个协程来划定碰撞框的出现和消失时间 void OnTriggerEnter2D(Collider2D other) { if(other.gameObject.CompareTag("Enemy")) { other.GetComponent<Enemy>().TakeDamage(damage); } }
我们为玩家物体创建一个子物体(如下图)作为玩家的攻击检测,其中找到玩家挥刀的那几帧(这里只需要打开Animation面板并选择Player找到其中的Attack动画即可对其进行编辑),并为显示出来的刀添加一个多边形碰撞体,这便是我们所需要的刀碰撞体检测,只有在挥刀的时候才出现该碰撞体,可以有效防止当玩家移动时即使没挥刀,刀碰到敌人也会造成伤害这一情况发生
2.敌人受伤
首先,关于敌人,敌人可以有很多种,例如蝙蝠、史莱姆、吸血鬼什么的,它们种类繁多,但都与共同的属性,比如受击掉血,攻击玩家,血空死亡等,因此,先创建一个“敌人”的父类是很有必要的,这有利于我们后面不同的敌人都可以继承父类中敌人的相同属性
1.受伤动画与掉血动画
很明显,受伤动画与掉血动画是每一个敌人的共有属性,因此,把这个放入敌人父类再合适不过了
public int damage; public float flashTime; public GameObject bloodEffect; public GameObject dropCoin; public GameObject floatPoint; private SpriteRenderer sr; private Color originalColor; private PlayerHealth playerHealth; public void TakeDamage(int damage) { FlashColor(flashTime);//让敌人闪烁 health -= damage; Instantiate(bloodEffect,transform.position,Quaternion.identity); //出现掉血特效 } public void Start() { sr = GetComponent<SpriteRenderer>(); originalColor = sr.color; } void FlashColor(float time) { sr.color = Color.red; Invoke("ResetColor", time); } void ResetColor() { sr.color = originalColor; }
其中,sr就是物体贴图的<SpriteRenderer>组件,我们可以在其中调节物体的颜色,相关的玩家攻击敌人的代码在刚才已经写过了,就在PlayerAttack脚本当中,如下
void OnTriggerEnter2D(Collider2D other) { if(other.gameObject.CompareTag("Enemy")) { other.GetComponent<Enemy>().TakeDamage(damage); } }
这样一来,就完成了两个脚本间的联动,玩家的刀光碰撞体触碰到敌人就会触发Enemy脚本当中的TakeDamage()方法,并执行相应的操作
那么,完成了敌人受伤变红一下的动画,接着就该是掉血动画了,这里我们要用到Unity的粒子系统,Particle System,我们在敌人身上创建一个空物体,并添加上粒子组件,调整适当的颜色和效果,这里以我自己的效果为例子
这便是先前代码中出现的BloodEffect,将其作为一个预制体,在触发攻击方法的时候实例化即可。与粒子效果相关的效果我会再出一篇相关文章,这里就不再赘述
2.玩家命中震动
震动的本质其实就是为摄像机添加一个位移动画,这里为方便演示,只作最基础的震动效果
我们创建一个camera的动画机,并创建一个idle状态,这是正常情况下的摄像机,此外,我们再新建一个摄像机震动的动画,名为CameraShake,并附上Trigger类型的Shake变量
后续的操作也不难,我们打开Camera的Animation,并调整到CameraShake动画当中,如图每隔一定时间Add Key,也就是右键添加关键帧,分别在每一个关键帧当中都对摄像机的Transform进行一个微小的改动,就如图所示,在我的示例当中,第一个关键帧不动,第二个关键帧x变为-0.1,第三个关键帧回到原点,第四个关键帧x变为0.1,这里不需要第五个关键帧,是因为当我的Shake动画播放完后,会回到Idle动画,也就是默认状态
public Animator cameraAnim; public void Shake() { cameraAnim.SetTrigger("Shake"); }
剩下的也很简单,给Camera创建一个CameraShake子物体,挂载以上脚本,然后回到我们的敌人脚本中的伤害方法,添加如下代码即可,
GameController.camShake.Shake();
其中GameController是为了把camShake变为一个静态的方法以直接用类名点出使用,并回到相机跟随脚本的Start方法进行声明
GameController.camShake = GameObject.FindGameObjectWithTag("CameraShake").GetComponent<CameraShake>(); //以上为在CameraFollow中的代码 //这样一来,我们可以直接在其他脚本当中直接调用GameController.camShake了 public class GameController : MonoBehaviour { public static CameraShake camShake; }
3.敌人简单AI
那么我们现在为我们场景中的蝙蝠创建一个脚本以控制其行为,当然,我们需要先让其继承自己的父类,以便获取Enemy脚本当中的一些属性
在这里我们所要做的简单AI,其实是让蝙蝠在我们划定的区域里随机飞行
public class EnemyBat : Enemy { public float speed; public float startWaitTime;//等待总时长 private float waitTime;//飞到目标位置后的当前等待时间 public Transform movePos; //飞行终点坐标 public Transform leftDownPos; public Transform rightUpPos; //在场景中的两个空物体:划定蝙蝠飞行范围的边界 public new void Start()//父类已经有了Start方法,因此这里需要关键字new修饰,Update同理 { base.Start(); waitTime = startWaitTime; movePos.position = GetRandomPos(); } public new void Update() { base.Update(); transform.position = Vector2.MoveTowards(transform.position, movePos.position, speed * Time.deltaTime); //前往终点坐标 if(Vector2.Distance(transform.position, movePos.position) < 0.1f)//两点间距离 { //这是为了让蝙蝠可以在空中停留一段时间 if(waitTime <= 0) { movePos.position = GetRandomPos(); waitTime = startWaitTime; } else { waitTime -= Time.deltaTime; } } Flip();//这个是之前写过的翻转方法 } Vector2 GetRandomPos()//获取一个随机坐标 { Vector2 randomPos = new Vector2(Random.Range(leftDownPos.position.x,rightUpPos.position.x),Random.Range(leftDownPos.position.y,rightUpPos.position.y)); //在范围内寻找一个随机的x,y值作为飞行终点坐标 return randomPos; } void Flip() //根据判断终点坐标在当前坐标的左侧或右侧决定是否进行翻转 { if(movePos.position.x - transform.position.x < 0) { Vector3 scale = transform.localScale; scale.x = -3; transform.localScale = scale; } else if (movePos.position.x - transform.position.x > 0) { Vector3 scale = transform.localScale; scale.x = 3; transform.localScale = scale; } } }
4.攻击伤害浮动
在我们玩过的许多游戏中,当我们攻击敌人,屏幕上就会浮现出我们造成的伤害数值
同样的,我们先创建空物体,附上动画机和新建一个伤害浮动动画,并添加上Mesh Renderer和Text Mesh组件,在其中写下我们造成的伤害数值,最后让动画中数字由下至上,由大至小。将其作为预制体,并在对敌人造成伤害时调用
Instantiate(floatPoint, transform.position, Quaternion.identity);
最后创建一个脚本规定数值消失的时间即可
void Start() { Destroy(gameObject, destoryTime); }
5.击败敌人爆金币
在先前的脚本当中,我们已经定义了敌人的血量,只需回到Enemy脚本中添加一个判断即可
public void Update() { if (health <= 0) { Instantiate(dropCoin, transform.position, Quaternion.identity); Destroy(gameObject); } }
6.玩家受伤与死亡
public class PlayerHealth : MonoBehaviour { public int health; public float hitBoxCDTime; private Animator anim; private PolygonCollider2D polygonCollider2D; //这里为玩家添加一个多边形碰撞体专门用来检测受击 void Start() { HealthBar.maxHealth = health; HealthBar.currentHealth = health; anim = GetComponent<Animator>(); polygonCollider2D = GetComponent<PolygonCollider2D>(); } public void DamagePlayer(int damage) { health -= damage; if(health < 0) { health = 0; } HealthBar.currentHealth = health; if(health <= 0) { anim.SetTrigger("Die"); Destroy(gameObject,1.0f); } anim.SetTrigger("Hurt"); } }
以上的DamagePlayer脚本也是与Enemy中的碰撞方法相对应
private void OnTriggerEnter2D(Collider2D other) { if (other.gameObject.CompareTag("Player") && other.GetType().ToString() == "UnityEngine.CapsuleCollider2D") //设定目标为玩家的多边形碰撞体 //如果只设置为玩家的话,玩家有多个碰撞体,会造成多次伤害 { if (playerHealth != null) { playerHealth.DamagePlayer(damage); } } }
7.玩家血条
public class HealthBar : MonoBehaviour { public Text healthText; public static int currentHealth; public static int maxHealth; private Image healthBar; void Start() { healthBar = GetComponent<Image>(); } public void Update() { healthBar.fillAmount = (float)currentHealth / maxHealth; //这里利用Image组件中的fill方法来进行血条的填充 healthText.text = currentHealth.ToString() + "/" + maxHealth.ToString(); } }
8.敌人追击玩家
public class EnemySmartBat : Enemy { public float speed; public float radius; private Transform playerTransform; public void Start() { base.Start(); playerTransform = GameObject.FindGameObjectWithTag("Player").GetComponent<Transform>(); //获取玩家位置 } void Update() { base.Update(); if(playerTransform != null) { float distance = (transform.position - playerTransform.position).magnitude; //magnitude是为了获取两个向量相减后的向量的长度 if(distance < radius)//进入攻击范围 { transform.position = Vector2.MoveTowards(transform.position,playerTransform.position,Time.deltaTime); //追击 } } Flip(); } void Flip()//与先前同样的翻转方法 { if (playerTransform != null) { if (playerTransform.position.x - transform.position.x < 0) { Vector3 scale = transform.localScale; scale.x = -3; transform.localScale = scale; } else if (playerTransform.position.x - transform.position.x > 0) { Vector3 scale = transform.localScale; scale.x = 3; transform.localScale = scale; } } } }
五、关于场景交互
1.实现毒气池
//该方法置于PlayerHealth脚本中,与受到敌人攻击共用一个多边形碰撞体 public void PosionDamagePlayer(int damage) { health -= damage; if (health < 0) { health = 0; } HealthBar.currentHealth = health; if (health <= 0) { anim.SetTrigger("Die"); Destroy(gameObject, 2.0f); } anim.SetTrigger("Hurt"); polygonCollider2D.enabled = false; StartCoroutine(ShowPlayerHitBox()); } IEnumerator ShowPlayerHitBox() { yield return new WaitForSeconds(hitBoxCDTime); polygonCollider2D.enabled = true; } //使用该协程是为防止玩家一下就被毒死 //正常情况下玩家都是隔一段时间才被毒一次
public class Posion : MonoBehaviour { public int damage; private PlayerHealth playerHealth; void Start() { playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerHealth>(); } void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag("Player") && other.GetType().ToString() == "UnityEngine.PolygonCollider2D") { playerHealth.PosionDamagePlayer(damage); } }
2.实现漂浮平台
public class MovePlatform : MonoBehaviour { public float speed; public float waitTime; public Transform[] movePos; //定义一个位置数组,用以存放漂浮平台的移动目标点 private int i = 1; private Transform transformOfPlayer; void Start() { transformOfPlayer = GameObject.FindGameObjectWithTag("Player").transform.parent; 找到Player标签物体的父物体,因为我们创建了一个Player空物体其子物体才是我们的Player本体 } void Update() { transform.position = Vector2.MoveTowards(transform.position, movePos[i].position, speed * Time.deltaTime); //控制漂浮平台在两个点之间移动 if(Vector2.Distance(transform.position, movePos[i].position) < 0.1f) //这是跟蝙蝠一样的等待方法 { if(waitTime < 0.0f) { if(i == 0) { i = 1; } else { i = 0; } waitTime = 0.5f; } else { waitTime -= Time.deltaTime; } } } private void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag("Player") && other.GetType().ToString() == "UnityEngine.BoxCollider2D") //检测玩家脚底的碰撞箱 { other.gameObject.transform.parent = gameObject.transform; //将玩家作为平台的子物体以达到玩家和平台一起移动的效果 } } private void OnTriggerExit2D(Collider2D other) { if (other.CompareTag("Player") && other.GetType().ToString() == "UnityEngine.BoxCollider2D") { other.gameObject.transform.parent = transformOfPlayer; //取消父子关系 } } }
3.实现单向跳跃平台
这个只需要用到unity的一个小组件,勾选其中的Use One Way便能达到单向跳跃的效果
想要运用这个组件还需要在碰撞体组件中勾选Use By Effector
而想要让玩家从单向跳跃平台中落下,只要将玩家的层和平台的层一致即可,这样一来,它们便不会发生碰撞,我们回到PlayerController脚本当中
void OneWayPlatformCheck() { if(isOneWayPlatform) { if (Input.GetKey(KeyCode.S) && Input.GetButtonDown("Jump")) { gameObject.layer = LayerMask.NameToLayer("OneWayPlatform"); anim.SetBool("Idle", false); anim.SetBool("Fall", true); Invoke("RestorePlayerLayer", restoreTime); //Invoke:稍后执行该方法 } } } void RestorePlayerLayer() { if(!isGround && gameObject.layer != LayerMask.NameToLayer("Player")) { gameObject.layer = LayerMask.NameToLayer("Player"); } }
4.实现捡起掉落物
捡起金币
注意我们需要两个碰撞箱,一个用来检测是否被玩家捡到,另一个则用以跟地面碰撞防止穿模
public class CoinItem : MonoBehaviour { private void OnTriggerEnter2D(Collider2D other) { if (other.gameObject.CompareTag("Player") && other.GetType().ToString() == "UnityEngine.CapsuleCollider2D") { CoinUI.currentCoinQuantity += 1; Destroy(gameObject); } } }
金币UI
public class CoinUI : MonoBehaviour { public int startCoinQuantity; public Text coinQuantity; public static int currentCoinQuantity; void Start() { currentCoinQuantity = startCoinQuantity; } void Update() { coinQuantity.text = currentCoinQuantity.ToString(); } }
5.实现告示牌
这也很简单,只需要为告示牌添加一个触发器
public class Sign : MonoBehaviour { public GameObject DialogBox; public Text dialogBoxText; public string signText; private bool isEnter; void Update() { if(Input.GetKeyDown(KeyCode.E) && isEnter) { dialogBoxText.text = signText; DialogBox.SetActive(true); } } void OnTriggerEnter2D(Collider2D other) { if(other.gameObject.CompareTag("Player") && other.GetType().ToString()=="UnityEngine.CapsuleCollider2D") { isEnter = true; } } void OnTriggerExit2D(Collider2D other) { if (other.gameObject.CompareTag("Player") && other.GetType().ToString() == "UnityEngine.CapsuleCollider2D") { isEnter = false; DialogBox.SetActive(false); } } }
6.实现风场
风场无需代码,仅仅需要一个组件
将力的角度设为90,大小为30,勾选碰撞体的Use By Effector便可以达到当玩家与碰撞体接触后便会触发Effector后受到一个方向竖直向上,大小为30N的力的效果
六、关于游戏
1.场景切换
无需多言
核心代码:
SceneManager.LoadScene("场景名字");
其中需要在Building Setting中拖入场景以确保可以转跳,在代码前需要引用命名空间
using UnityEngine.SceneManagement;
2.游戏菜单
public class PauseMenu : MonoBehaviour { public static bool gameIsPaused = false; public GameObject PauseMenuUI; void Update() { if(Input.GetKeyDown(KeyCode.Escape)) { if(gameIsPaused) { Resume(); } else { Pause(); } } } public void Resume() { PauseMenuUI.SetActive(false); Time.timeScale = 1.0f; gameIsPaused = false; } public void Pause() { PauseMenuUI.SetActive(true); Time.timeScale = 0.0f; gameIsPaused = true; } }
再为按钮添加点击事件即可完成
——————————————————————————————————————————
此文章为根据该视频教学进行的归纳总结https://www.bilibili.com/video/BV1sE411L7kV/?spm_id_from=333.999.0.0