文章目录
0.写在前面
此篇博客用来记录游戏开发过程中遇到的各种问题以及大致的流程,方便自己以后回顾学习,如果想要了解完整的开发过程,最好去看教学视频。
U n i t y Unity Unity版本: 2019.4.12 f 1 2019.4.12f1 2019.4.12f1。
相应的视频教程请在 B B B站搜索 u p up up主 M _ S t u d i o M\_Studio M_Studio观看。
1.导入素材
在商店中搜索 S u n n y L a n d Sunny\ Land Sunny Land即可找到对应的资源,下载后导入到当前项目即可。
2.编辑素材&Tilemap
在编辑背景等图片资源之前有一个知识点需要我们了解,那就是单位格:
上图所示的每一个小方格都是单位格,而图片资源会有如下属性:
通过修改这一参数(单位长度内的像素个数),我们就可以控制图片资源显示的大小。该项目内统一使用 16 16 16。
下面简单介绍一下 T i l e m a p Tilemap Tilemap是什么,怎么用。我们可以先看一张图片:
简单来说,利用 T i l e m a p Tilemap Tilemap我们可以快速的创建 2 D 2D 2D地图。通过对 S p r i t e Sprite Sprite也就是图片的切分,我们可以得到一个个 T i l e Tile Tile,他们就相当于颜色;我们可以把它们附着在 P a l e t t e Palette Palette上面(调色板);然后通过 B r u s h Brush Brush在 T i l e m a p Tilemap Tilemap也就是场景中绘制,就像画画一样。那么在 u n i t y unity unity中整个流程是怎么样的呢?
1. 1. 1.切分图片。
2. 2. 2.创建 P a l e t t e Palette Palette。
3. 3. 3.将 T i l e Tile Tile添加到 P a l e t t e Palette Palette中(拖入图片创建 T i l e Tile Tile)。
4. 4. 4.在场景中创建 T i l e m a p Tilemap Tilemap。
5. 5. 5.通过 B r u s h Brush Brush把 T i l e Tile Tile绘制到 T i l e m a p Tilemap Tilemap中。
如果在 g a m e game game视图下看到场景中有缝隙,可以修改这个参数:
3.图层layer&角色建立
有时候我们需要控制场景中不同物体的渲染顺序,比如地形之类的应该在背景之上显示,这个该怎么做呢? S o r t i n g L a y e r 、 O r d e r i n L a y e r Sorting\ Layer、Order\ in\ Layer Sorting Layer、Order in Layer。前者选中的 l a y e r layer layer越靠下,显示的越靠上;同一 l a y e r layer layer中,后者的值越大,显示的越靠上。
想要创建人物,首先要在场景中创建一个精灵,然后修改它所引用的图片。同时不要忘了修改位置、层级以及图片的大小( P i x e l s p e r u n i t Pixels\ per\ unit Pixels per unit)。
但是此时我们的人物还不会动,也不受重力影响。所以我们需要为其加上 r i g i d b o d y 2 D rigidbody2D rigidbody2D和碰撞体组件,当然,场景中的地形也需要加上碰撞体。
4.角色移动
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
private Rigidbody2D rb2d;
public float speed = 5f;
// Start is called before the first frame update
void Start()
{
rb2d = GetComponent<Rigidbody2D>();
}
// Update is called once per frame
void Update()
{
Movement();
}
void Movement()
{
float horizontalMove = Input.GetAxis("Horizontal");
rb2d.velocity = new Vector2(horizontalMove * speed, rb2d.velocity.y);
}
}
上述代码即可实现简单的人物移动,但是在测试中你可能会发现人物会出现摔倒等现象,这是由于发生碰撞时人物绕 z z z轴旋转了,所以我们可以冻结这一旋转:
5.角色方向&跳跃
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
private Rigidbody2D rb2d;
public float speed = 5f;
public float jumpForce = 5f;
// Start is called before the first frame update
void Start()
{
rb2d = GetComponent<Rigidbody2D>();
}
// Update is called once per frame
void Update()
{
Movement();
}
void Movement()
{
float horizontalMove = Input.GetAxis("Horizontal");
int faceDirection = (int)Input.GetAxisRaw("Horizontal");
rb2d.velocity = new Vector2(horizontalMove * speed, rb2d.velocity.y);
if (faceDirection != 0)
{
transform.localScale = new Vector3(faceDirection, 1, 1);
}
if (Input.GetButtonDown("Jump"))
{
rb2d.velocity = new Vector2(rb2d.velocity.x, jumpForce);
}
}
}
上述代码实现了人物的翻转、跳跃。翻转的做法有很多种,除了修改 s c a l e scale scale还可以通过修改这面这个属性做到:
但是这两种方法不是一直都等价的,考虑人物的背上还有装备的情况,那么一般情况下它们是父子关系,此时修改父物体的 s c a l e scale scale,子物体也会一同修改,但是上面这个 F l i p Flip Flip是独立的,所以这种情况下想要实现翻转的话应该利用 s c a l e scale scale。
由于我们还没有做地面检测,所以人物可以一直跳。
6.动画效果Animation
给人物添加 A n i m a t o r Animator Animator组件之后就可以开始制作动画了~
通过这个设置可以查看当前动画的帧率并进行设置。
按住左键不松可以选取一定区域内的所有关键帧,或者先选中一个关键帧,此时按住 s h i f t shift shift不松再选择另一个关键帧,即可自动选中它们之间的所有关键帧( u n i t y unity unity中选择多个文件的方法同理),此时再拖动(选中区域的两侧有竖线标识)即可均匀的设置间隔而无需一个一个的调整。
在制作好 i d l e idle idle和 r u n run run两段动画后,就可以开始修改 a n i m a t o r animator animator了。我们需要为这两段动画之间加一个过渡条件:
若勾选 H a s E x i t T i m e Has\ Exit\ Time Has Exit Time,则代表从动画 A A A到动画 B B B有特定的退出(转换)时间,具体值由 E x i t T i m e Exit\ Time Exit Time决定,也就是说切换可能会有延迟(不能立即切换); T r a n s i t i o n D u r a t i o n Transition\ Duration Transition Duration决定了过渡的时间,即动作 A A A经过多久可以过渡到动作 B B B。这里,我们既不需要退出时间,也不需要过渡时间。代码只需要改动一点点,修改 r u n n i n g running running的值即可。
void Movement()
{
animator.SetFloat("running", Mathf.Abs(horizontalMove));
}
7.跳跃动画 LayerMask
跳跃动画需要分成两部分: 1. 1. 1.起跳; 2. 2. 2.下落,所以我们需要制作两个动画,然后修改状态机:
现在考虑代码怎么改。起跳是很简单的,只需要在用户按下空格键时,将 j u m p i n g jumping jumping设为真;降落也比较简单,当人物在跳跃状态且 y y y方向的速度 < 0 <0 <0时,说明人物开始降落了,那么此时应将 j u m p i n g jumping jumping设为假, f a l l i n g falling falling设为真;关键是如何判断人物落地了呢?通过碰撞来判断。在这里我们引入 L a y e r M a s k LayerMask LayerMask,它在射线检测中经常被用到,用来实现与特定层的检测(忽略其它层)。那么我们自然想到可以将地面的 l a y e r layer layer设置为 g r o u n d ground ground,然后在代码中判断人物的碰撞体是否与 g r o u n d ground ground接触即可:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
private Rigidbody2D rb2d;
private Animator animator;
private BoxCollider2D boxCollider2D;
public float speed = 5f;
public float jumpForce = 5f;
public LayerMask ground;
// Start is called before the first frame update
void Start()
{
rb2d = GetComponent<Rigidbody2D>();
animator = GetComponent<Animator>();
boxCollider2D = GetComponent<BoxCollider2D>();
}
// Update is called once per frame
void Update()
{
Movement();
SwitchAnimation();
}
void Movement()
{
float horizontalMove = Input.GetAxis("Horizontal");
int faceDirection = (int)Input.GetAxisRaw("Horizontal");
rb2d.velocity = new Vector2(horizontalMove * speed, rb2d.velocity.y);
animator.SetFloat("running", Mathf.Abs(horizontalMove));
if (faceDirection != 0)
{
transform.localScale = new Vector3(faceDirection, 1, 1);
}
if (Input.GetButtonDown("Jump"))
{
rb2d.velocity = new Vector2(rb2d.velocity.x, jumpForce);
animator.SetBool("jumping", true);
}
}
void SwitchAnimation()
{
//animator.SetBool("idle", false);
if (animator.GetBool("jumping")) //跳跃状态
{
if (rb2d.velocity.y < 0)
{
animator.SetBool("jumping", false);
animator.SetBool("falling", true);
}
}
else if (animator.GetBool("falling")) //下落状态
{
if (boxCollider2D.IsTouchingLayers(ground))
{
animator.SetBool("falling", false);
animator.SetBool("idle", true);
}
}
}
}
上述代码实现了人物各种动作之间的切换。你可能注意到了上述代码中的一些问题,当 i d l e idle idle被设为真后,就一直为真,但是这并不影响动作的切换,因为到目前为止 i d l e idle idle其实没有存在的必要,但是为了与教程统一,我还是加入了这个变量。所以大家不要盲目的相信教程全都是正确的,从教程中学到方法,然后自己思考怎么做。
8.修复移动错误&Body Type&Composite Collider
人物在移动时可能出现卡住不动的情况,这个应该是碰撞体的问题。在 s c e n e scene scene视图下我们可以看到当前地形( T i l e m a p Tilemap Tilemap)的碰撞体:
可以发现每一个方格都有自己的碰撞体,然而很多地方是没有必要的,比如地形的内部。所以我们需要使用 c o m p o s i t e c o l l i d e r composite\ collider composite collider来把这些碰撞体合并到一起,具体做法就是为 t i l e m a p tilemap tilemap添加一个对应的组件:
可以看到地形内部的碰撞体消失了(被合并成了一个整体),这样做不仅可以减少计算量,提高性能,还可以解决人物移动时突然卡住的 b u g bug bug。但是如果这个时候运行游戏,你会发现地形和人物一起掉落了,这是因为 c o m p o s i t e c o l l i d e r 2 D composite\ collider\ 2D composite collider 2D需要 r i g i d b o d y 2 D rigidbody\ 2D rigidbody 2D,所以地形也会受到重力影响,这个问题怎么解决呢?首先我们来了解一下 r i g i d b o d y 2 D rigidbody\ 2D rigidbody 2D的 b o d y t y p e body\ type body type。
显然,我们是不希望地形进行移动的,所以应该选取 s t a t i c static static。
9.镜头控制Cinemachine
C i n e m a c h i n e Cinemachine Cinemachine是一个非常棒的插件,利用它可以快速制作出满足我们需求的摄像机——跟随人物缓慢移动。首先我们需要把背景拉长,多复制几份即可:
注意这里有一个小技巧,在利用移动工具移动游戏对象时,按住 V V V会在鼠标附近的区域出现如上图所示的白色方框,拖动白色方框移动游戏对象则会使它附着在附近的其它游戏对象上,也就是说可以自动进行对齐等操作,更加方便。
修改完背景后,就可以添加 C i n e m a c h i n e Cinemachine Cinemachine摄像机了,并且让它跟随我们的人物。
从上图可以看到这个摄像机分为三个区域,人物在最中间的区域移动时,摄像机不会跟随;在浅蓝色区域移动时,摄像机缓慢跟随;在红色区域移动时,摄像机快速跟随。右边红框内的参数可以调节这几个区域的大小。现在还有一个问题就是,当相机随着人物移动到边界时,会看到超出背景的部分,这是我们不希望的。但是这个摄像机已经为我们考虑到了:
我们只需要给背景增加一个 p o l y g o n c o l l i d e r 2 D polygon\ collider\ 2D polygon collider 2D(编辑这种碰撞体时,按住 c t r l ctrl ctrl可以删除某一个点),然后把它拖到上面的 B o u n d i n g Bounding Bounding即可。注意把背景的碰撞体设置成触发器,不然会把人物顶出去。
10.物品收集 & Perfabs
仿照人物的制作方式,自己动手制作樱桃,并修改其 t a g tag tag:
修改代码使得人物碰到樱桃时,樱桃消失(之前写的代码省略掉了):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
[SerializeField]
private int cherry = 0;
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == "Collection")
{
Destroy(collision.gameObject);
++cherry;
}
}
}
新建 P r e f a b Prefab Prefab文件夹,把人物和樱桃都设置为预制体:
接下来大家可以自己发挥,把场景设置的更加丰富~
11.物理材质&空中跳跃
相信大家在游玩的过程中可能已经发现了一些问题: 1. 1. 1.人物碰到某个物体后如果一直按着方向键,那么人物不会落下,会卡在物体上; 2. 2. 2.人物可以无限跳跃。现在我们就来解决这些问题。
首先来解决第一个问题,为什么按着方向键就会卡住,不按就可以正常落下呢?因为摩擦力(真实的物理引擎 )。所以我们要做的就是修改人物碰撞体的材质:
然后来解决第二个问题,只要在代码中进行控制,当人物位于地面上时才允许跳跃就行了:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
void Movement()
{
if (Input.GetButtonDown("Jump") && boxCollider2D.IsTouchingLayers(ground))
{
rb2d.velocity = new Vector2(rb2d.velocity.x, jumpForce);
animator.SetBool("jumping", true);
}
}
不过此时人物依然可以蹬墙跳。
12.UI入门
想让界面显示出任务获得的樱桃个数,那么就需要用到 U I UI UI了:
图片和分数我们都想放到屏幕左上角,那么应该通过设置锚点来定位。如果单单使用到中心点的偏移的话,当屏幕大小发生变化时, U I UI UI极有可能出现在错误的位置。
分数的控制由代码实现:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class PlayerController : MonoBehaviour
{
public Text cherryText;
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == "Collection")
{
Destroy(collision.gameObject);
++cherry;
cherryText.text = cherry.ToString();
}
}
}
13.敌人Enemy!
首先仿照人物的制作方法制作出一个青蛙敌人,然后将其设为预制体:
青蛙的碰撞体不能当作触发器使用,因为它有 r i g i d b o d y 2 D rigidbody\ 2D rigidbody 2D组件,当成触发器的话会直接掉落。我们希望人物跳起来落到青蛙头上时可以消灭它,同时有一个小跳的效果:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class PlayerController : MonoBehaviour
{
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Enemy" && animator.GetBool("falling"))
{
Destroy(collision.gameObject);
rb2d.velocity = new Vector2(rb2d.velocity.x, jumpForce); //小跳效果
animator.SetBool("jumping", true);
}
}
}
不过这里还是存在一些问题的,由于当前的动画设置,人物从高处落下时并不一定处于降落状态,可以考虑把这里的判断条件改为人物的 v e l o c i t y . y < 0 velocity.y<0 velocity.y<0。
14.受伤效果Hurt
首先按照教程把修改代码如下所示:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class PlayerController : MonoBehaviour
{
[SerializeField]
private bool isHurt = false;
// Update is called once per frame
void Update()
{
if (!isHurt)//非受伤状态
{
Movement();
}
SwitchAnimation();
}
void SwitchAnimation()
{
//animator.SetBool("idle", false);
if (animator.GetBool("jumping")) //跳跃状态
{
if (rb2d.velocity.y < 0)
{
animator.SetBool("jumping", false);
animator.SetBool("falling", true);
}
}
else if (animator.GetBool("falling")) //下落状态
{
if (boxCollider2D.IsTouchingLayers(ground))
{
animator.SetBool("falling", false);
animator.SetBool("idle", true);
}
}
else if (isHurt) //受伤状态
{
if (Mathf.Abs(rb2d.velocity.x) < 0.1f)//速度过小时认为回到正常状态
{
isHurt = false;
}
}
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Enemy")
{
if (animator.GetBool("falling")) //掉落状态
{
Destroy(collision.gameObject);
rb2d.velocity = new Vector2(rb2d.velocity.x, jumpForce); //小跳效果
animator.SetBool("jumping", true);
}
else if (transform.position.x < collision.gameObject.transform.position.x)//左侧
{
isHurt = true;
rb2d.velocity = new Vector2(-10f, rb2d.velocity.y);
}
else if(transform.position.x > collision.gameObject.transform.position.x)//右侧
{
isHurt = true;
rb2d.velocity = new Vector2(10f, rb2d.velocity.y);
}
}
}
}
但是在运行后我发现了一个问题,就是碰撞后人物会一直后退直到碰到其它碰撞体。这是因为教程中使用了两个碰撞体,而我只使用了一个而且把摩擦力改成了 0 0 0,所以我决定通过代码控制受伤状态的速度:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class PlayerController : MonoBehaviour
{
[SerializeField]
private bool isHurt = false;
// Update is called once per frame
void Update()
{
if (!isHurt)//非受伤状态
{
Movement();
}
SwitchAnimation();
}
void SwitchAnimation()
{
//animator.SetBool("idle", false);
if (animator.GetBool("jumping")) //跳跃状态
{
if (rb2d.velocity.y < 0)
{
animator.SetBool("jumping", false);
animator.SetBool("falling", true);
}
}
else if (animator.GetBool("falling")) //下落状态
{
if (boxCollider2D.IsTouchingLayers(ground))
{
animator.SetBool("falling", false);
animator.SetBool("idle", true);
}
}
else if (isHurt) //受伤状态
{
int sign = rb2d.velocity.x < 0 ? -1 : 1;
rb2d.velocity += new Vector2(speed * Time.deltaTime, 0f) * -sign;
if (Mathf.Abs(rb2d.velocity.x) < 0.1f)
{
isHurt = false;
}
}
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Enemy")
{
if (animator.GetBool("falling")) //掉落状态
{
Destroy(collision.gameObject);
rb2d.velocity = new Vector2(rb2d.velocity.x, jumpForce); //小跳效果
animator.SetBool("jumping", true);
}
else if (transform.position.x < collision.gameObject.transform.position.x)//左侧
{
isHurt = true;
rb2d.velocity = new Vector2(-5f, rb2d.velocity.y);
}
else if(transform.position.x > collision.gameObject.transform.position.x)//右侧
{
isHurt = true;
rb2d.velocity = new Vector2(5f, rb2d.velocity.y);
}
}
}
}
下一步就是制作受伤时的动画了:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class PlayerController : MonoBehaviour
{
void SwitchAnimation()
{
//animator.SetBool("idle", false);
if (animator.GetBool("jumping")) //跳跃状态
{
if (rb2d.velocity.y < 0)
{
animator.SetBool("jumping", false);
animator.SetBool("falling", true);
}
}
else if (animator.GetBool("falling")) //下落状态
{
if (boxCollider2D.IsTouchingLayers(ground))
{
animator.SetBool("falling", false);
animator.SetBool("idle", true);
}
}
else if (isHurt) //受伤状态
{
animator.SetBool("hurt", true);
int sign = rb2d.velocity.x < 0 ? -1 : 1;
rb2d.velocity += new Vector2(speed * Time.deltaTime, 0f) * -sign;
if (Mathf.Abs(rb2d.velocity.x) < 0.1f)
{
isHurt = false;
animator.SetBool("hurt", false);
}
}
}
15.AI敌人移动
这里教程采用的方法是在青蛙左右两侧设置两个点,把青蛙的移动区域固定到这两个点内。我这里没有采用这种做法,而是使用了 P h y s i c s 2 D . L i n e c a s t Physics2D.Linecast Physics2D.Linecast判断青蛙前方是否是空地,更具体一点就是判断青蛙脚下是不是地面,以及青蛙头上有没有障碍物:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy_Frog : MonoBehaviour
{
private Rigidbody2D rb2d;
public LayerMask ground;
public float speed = 5f;
[SerializeField]
private bool faceLeft = true;
// Start is called before the first frame update
void Start()
{
rb2d = GetComponent<Rigidbody2D>();
}
// Update is called once per frame
void Update()
{
Movement();
}
void Movement()
{
Vector2 frontPosition = transform.position;
if (faceLeft)
frontPosition += Vector2.left;
else
frontPosition += Vector2.right;
if (!Physics2D.Linecast(frontPosition + Vector2.down, frontPosition, ground) || Physics2D.Linecast(frontPosition, frontPosition + Vector2.up, ground)) //没有检测到地面 或者头上有障碍物
{
faceLeft = !faceLeft;
transform.localScale = new Vector3(faceLeft ? 1 : -1, 1, 1); //角色反向
}
rb2d.velocity = Vector2.right * (faceLeft ? -speed : speed);
}
private void OnDrawGizmos() //debug用
{
Vector2 frontPosition = transform.position;
if (faceLeft)
frontPosition += Vector2.left;
else
frontPosition += Vector2.right;
Gizmos.DrawLine(frontPosition + Vector2.down, frontPosition + Vector2.up);
}
}