一、2D渲染
1、2D相机基本设置
上面是透视,下面是正交
2、图片资源
在Unity中,常规图片导入之后,一般不在Unity中直接使用,而是转为精灵图Sprite
将图片更改为即可使用Unity内置的图片切割功能
无论精灵图片是单个的还是多个的,都可以选择中心点
3、相关组件
在排序图层中,越靠下的,越先渲染
也就是说,靠近摄像机的因该是最下方的图层
二、Unity输入系统
1、按键检测
关于键盘的
if (Input.GetKeyDown(KeyCode.A))
{
Debug.Log("A按下了!");
}
if (Input.GetKey(KeyCode.A))
{
Debug.Log("A持续按住!");
}
if (Input.GetKeyUp(KeyCode.A))
{
Debug.Log("A弹起了!");
}
2、鼠标检测
和键盘区别不大,只是调用的API不同
注:0是左键;1是右键;2是鼠标中键
if (Input.GetMouseButtonDown(0))
{
Debug.Log("鼠标左键按下");
}
if (Input.GetMouseButton(0))
{
Debug.Log("鼠标左键按住");
}
if (Input.GetMouseButtonUp(0))
{
Debug.Log("鼠标左键抬起");
}
3、InputManager
在Unity的设置中找到输入部分,即可看到默认绑定的按键
一个简单的移动代码
float inputX = Input.GetAxis("Horizontal");
float inputY = Input.GetAxis("Vertical");
float speed = 5;
transform.Translate(Time.deltaTime * speed * new Vector3(inputX, inputY, 0));
注:这里的速度乘以时间,放在前面,避免一些不必要的消耗
三、Unity2D动画概览
1、Animator组件
先在窗口上添加一个动画组件
一定是下面这个。上面这个是比较老的技术了
2、AnimatorController:动画控制器
3、AnimationClip:动画片段
四、动画录制
在Unity内制作动画
在录制时,必须开启录制按钮,可以选择数据,也可以修改数据,录制时会自动记录下来
也可以添加颜色
五、动画切换
通过参数进行切换,小于或者大于
2D人物的简单移动代码
public Animator animator;
public float movespeed = 0.00001f;
public SpriteRenderer spriteRenderer;
private void Start()
{
spriteRenderer= gameObject.GetComponent<SpriteRenderer>();
}
// Update is called once per frame
void Update()
{
animator.GetComponent<Animator>();
float inputX = Input.GetAxis("Horizontal");
float inputY = Input.GetAxis("Vertical");
//修改动画
bool isMove = inputX != 0 || inputY!=0;
animator.SetBool("Move", isMove);
//转向
if (inputX > 0) spriteRenderer.flipX = false;
else if (inputX < 0) spriteRenderer.flipX = true;
//位移
Vector3 pos =
new Vector3(inputX * Time.deltaTime * movespeed,
inputY * Time.deltaTime * movespeed,
0);
transform.Translate(pos, Space.Self);
}
六、动画事件
在动画的某一帧触发某个函数;
原理是:在当前动画物体上的脚本里面 是否有一个事件;必须是公开的
在此处添加动画事件;
选择动画事件代码
public void Colore()
{
transform.localScale=new Vector3(3, 3, 3);
Debug.Log("1");
}
执行到这个事件时,会执行这个函数
七、2D物理系统
每个人都需要有碰撞器,只要有一个有刚体
事件触发函数
private void OnCollisionEnter2D(Collision2D collision)
{
}
private void OnCollisionStay2D(Collision2D collision)
{
}
private void OnCollisionExit2D(Collision2D collision)
{
}
碰撞与触发不能共存
触发函数代码
private void OnTriggerEnter2D(Collider2D collision)
{
}
private void OnTriggerStay2D(Collider2D collision)
{
}
private void OnTriggerExit2D(Collider2D collision)
{
}
八、主角控制(综合案例)
在玩家身上挂载一个触发器,不需要碰撞
挂载脚本
public class Game : MonoBehaviour
{
public Animator animator;
public float movespeed;
public SpriteRenderer spriteRenderer;
bool Big=false;
private void Start()
{
spriteRenderer= gameObject.GetComponent<SpriteRenderer>();
}
// Update is called once per frame
void Update()
{
animator.GetComponent<Animator>();
float inputX = Input.GetAxis("Horizontal");
float inputY = Input.GetAxis("Vertical");
//修改动画
bool isMove = inputX != 0 || inputY!=0;
animator.SetBool("Move", isMove);
//转向
if (inputX > 0) spriteRenderer.flipX = false;
else if (inputX < 0) spriteRenderer.flipX = true;
//位移
Vector3 pos =
new Vector3(inputX * Time.deltaTime * movespeed,
inputY * Time.deltaTime * movespeed,
0);
transform.Translate(pos, Space.Self);
}
}
由于人物比较小,调节一下摄像机距离
九、发射子弹
子弹一般都是预制体;
需要一个公开获取预制体;一个生成子弹的点位;一个if循环判断按键和实例化
子弹飞行是自己的逻辑;挂载在预制体上面;
private void Shoot()
{
if (Input.GetKeyDown(KeyCode.J))
{
//实例化
GameObject star=GameObject.Instantiate(starPrefab);
star.transform.position = starShootPoint.position;
}
}
创建一个动画,模拟子弹射出
旋转360° 然后旋转720°
将动画勾选为循环
在预制体子弹上面挂在一个动画状态机,放入旋转动画
挂载子弹的代码
public float moveSpeed;
private Vector3 moveDir;
public float destroyTime=3;
private float destroyTimer;//计时器
public void Init(Vector3 dir)
{
moveDir = dir;
destroyTimer = destroyTime;
}
// Update is called once per frame
void Update()
{
transform.Translate(moveSpeed * Time.deltaTime * moveDir,Space.World);
if (destroyTimer <= 0)
{
Destroy(gameObject);
}else
{
destroyTimer-=Time.deltaTime;
}
}
由于子弹自身有旋转,所以坐标会一直更改,需要传入一个方向初始化
在玩家代码中调用
//实例化
GameObject star=GameObject.Instantiate(starPrefab);
star.transform.position = starShootPoint.position;
bool isRightDir = spriteRenderer.flipX == false;
Vector3 moveDir = isRightDir ? transform.right : -transform.right;
Debug.Log(moveDir);
star.GetComponent<Star>().Init(moveDir);
先实例化这个预制体;把生成点赋值给预制体的当前坐标
通过bool判断是左还是右
然后获取预制体代码中的初始化方法,并传入方向
然后写一个计时器,让子弹销毁
public class Game : MonoBehaviour
{
public Animator animator;
public float movespeed;
public SpriteRenderer spriteRenderer;
public GameObject starPrefab;
public Transform starShootPoint;
//CD
private float shootCD = 1;
private float shootCDTimer;
private void Start()
{
spriteRenderer= gameObject.GetComponent<SpriteRenderer>();
}
// Update is called once per frame
void Update()
{
Move(); Shoot();
}
private void Move()
{
animator.GetComponent<Animator>();
float inputX = Input.GetAxis("Horizontal");
float inputY = Input.GetAxis("Vertical");
//修改动画
bool isMove = inputX != 0 || inputY != 0;
animator.SetBool("Move", isMove);
//转向
if (inputX > 0) spriteRenderer.flipX = false;
else if (inputX < 0) spriteRenderer.flipX = true;
//位移
Vector3 pos =
new Vector3(inputX * Time.deltaTime * movespeed,
inputY * Time.deltaTime * movespeed,
0);
transform.Translate(pos, Space.Self);
}
private void Shoot()
{
if (shootCDTimer > 0)
{
shootCDTimer-= Time.deltaTime;
return;
}
if (Input.GetKeyDown(KeyCode.J))
{
shootCDTimer = shootCD;
//实例化
GameObject star=GameObject.Instantiate(starPrefab);
star.transform.position = starShootPoint.position;
bool isRightDir = spriteRenderer.flipX == false;
Vector3 moveDir = isRightDir ? transform.right : -transform.right;
Debug.Log(moveDir);
star.GetComponent<Star>().Init(moveDir);
}
}
}
十、怪物生成
1、配置一些坐标生成怪物
创建一个空物体,挂载怪物的代码
对怪物的基础配置进行设置:
这里有一个方法;使用class类,然后把这个类作为一个数组,公开这个数组为public即可
/// <summary>
/// 怪物生成点
/// </summary>
[Serializable]
public class MonsterSpawnPoint
{
public Transform spawnPoint;//生成点
public GameObject monsterPrefab;//怪物预制体
public float spawnInterval;//怪物生成间隔
//这里的计时器不想序列化,但是需要获取,所以改为属性
public float SpawnTimer { get; set; }
}
public MonsterSpawnPoint[] monsterSpawnPoints;
小技巧:把动画片段全部选中拖拽进入层级面板中,可以快速地生成动画和动画状态
2、按照一个频率生成怪物
现在面板上设置了预制体和生成间隔
我们做一个计时器,计时器命名为Timer
用一个for循环遍历这5个数组
声明一个局部变量类,让每一次循环都赋值给这个局部变量类,也就是每一次循环Point都获取了数组中的参数;
使用计时器,先让他小于零;
做一个判断如果小于零,那么就把生成间隔赋值给计时器;
并且实例化类中的预制体,把类中的生成点赋值给怪物的当前位置;
private void Update()
{
for (int i=0;i< monsterSpawnPoints.Length;i++)
{
MonsterSpawnPoint point= monsterSpawnPoints[i];
point.SpawnTimer-=Time.deltaTime;//这里的计时器一开始就会小于零
if (point.SpawnTimer <= 0)//一定会小于零
{
point.SpawnTimer = point.spawnInterval;//就把生成间隔赋值给计时器
GameObject monster = GameObject.Instantiate(point.monsterPrefab);
monster.transform.position = point.spawnPoint.position;
}
}
}
现在对代码进行一下优化,
public class MonsterManager : MonoBehaviour
{
/// <summary>
/// 怪物生成点
/// </summary>
[Serializable]
public class MonsterSpawnPoint
{
public Transform spawnPoint;//生成点
public GameObject monsterPrefab;//怪物预制体
public float spawnInterval;//怪物生成间隔
//这里的计时器不想序列化,但是需要获取,所以改为属性
public float SpawnTimer { get; set; }
public void Update(float deltaTime)
{
SpawnTimer -= Time.deltaTime;//这里的计时器一开始就会小于零
if (SpawnTimer <= 0)//一定会小于零
{
SpawnTimer = spawnInterval;//就把生成间隔赋值给计时器
GameObject monster = GameObject.Instantiate(monsterPrefab);
monster.transform.position = spawnPoint.position;
}
}
}
public MonsterSpawnPoint[] monsterSpawnPoints;
private void Update()
{
for (int i=0;i< monsterSpawnPoints.Length;i++)
{
monsterSpawnPoints[i].Update(Time.deltaTime);
}
}
}
因为update中调用其他类中的变量,不如直接调用update封装起来
所以在Update中定义一个参数(时间),因为调用这个update传入的参数是Time.deltatime
把生成的逻辑封装起来在Update中
在主update中,每一个for循环之后,数组的第i次循环调用这个UPdate,并传入每时每秒。
十一、怪物移动逻辑
一直去追玩家,如果解除了玩家则玩家失败;
首先把玩家设置成单例;在怪物类中先设置一个判断;判断玩家是否为空,也就是死亡状态
if (Game.Instance == null) return;
计算玩家到敌人的距离,方法是使用向量,用敌人的当前坐标 减去 自己的当前坐标
Vector3 moveDir=Game.Instance.transform.position-transform.position;
然后进行位移
transform.Translate(moveSpeed * Time.deltaTime * moveDir,Space.World);
此时敌人背面朝着敌人移动,所以使用Bool判断玩家在左或者右
如果玩家的坐标小于怪物的坐标,说明是在左侧,那么更改怪物的缩放值X
bool isRight=transform.position.x>Game.Instance.transform.position.x;
if (isRight) transform.localScale = new Vector3(-1, 1, 1);
else transform.localScale = new Vector3(1, 1, 1);
十二、子弹消灭怪物
1、怪物被攻击,减少血量
首先,把死亡的动画片段拖进两个敌人的动画控制器中
我选择使用触发 作为死亡的条件
//接触子弹
if (collision.gameObject.CompareTag("Bullet"))
{
Destroy(collision.gameObject);//取消子弹穿透
hp -= 1;
if (hp == 0)
{
animator.SetTrigger("Death");
}
}
如果血量为零,则播放死亡动画
2、血量为0,播放死亡动画,销毁自己
#region 动画事件
private void OnDeathAnimationEnd()
{
Destroy(gameObject);
}
#endregion
在死亡动画的最后一帧执行销毁
十三、鼠标指向为攻击方向
使用屏幕坐标 到 世界坐标
public new Camera camera;
Vector3 mouseWorldPosition=camera.ScreenToWorldPoint(Input.mousePosition);
获取到的屏幕坐标,因为两个方法都需要,所以作为参数传进来
十四、玩家生命值以及状态
不适用UI;
把血条作为摄像机的子物体,防止血条跟随摄像机改变位置;
不让摄像机在玩家的子物体下,通过代码跟随
使用LateUpdate,让摄像机慢一帧追逐玩家
public Transform target;
private void LateUpdate()
{
transform.position = target.position;
}
此时,出现问题。
因为2d的,摄像机到玩家的距离是-10,运行后摄像机与玩家重合会看不见
修改代码,固定Z轴坐标
Vector3 pos=target.position;
pos.z = -10;
transform.position = pos;
因为血条是几张图片切换实现的
所以,在玩家身上公开一个变量HP,把Hp设置为字段,get无所谓,set设置切换
[SerializeField]private int hp;
public int Hp
{
get => hp;
set
{
hp = value;
}
}
然后引用血量图,做成一个数组
public SpriteRenderer hpSpriteRenderer;
public Sprite[] spriteRenderers;
然后在字段中,把精灵图第几个赋值给血条上
public int Hp
{
get => hp;
set
{
hp = value;
hpSpriteRenderer.sprite = hpSprites[hp];
}
}
初始化中,把数组长度作为血量值传进来
private void Awake()
{
Instance = this;
Hp = hpSprites.Length-1;
}
十五、怪物干掉玩家
怪物碰到玩家,掉血,玩家掉血为0则死亡
在此处遇到问题:怪物与玩家无法碰撞,碰撞代码逻辑未执行;
原因是刚体的数值未设置正确。
public class CameraController : MonoBehaviour
{
public Transform target;
private Game game1;
private void Awake()
{
game1 = FindObjectOfType<Game>();
if (game1 == null)
{
Debug.Log("没找到");
}
}
private void LateUpdate()
{
int xue = game1.Hp;
Debug.Log(xue);
if (xue == 0)
{
return;
}
Vector3 pos=target.position;
pos.z = -10;
transform.position = pos;
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//using UnityEngine.Windows;
public class Game : MonoBehaviour
{
public static Game Instance;
public Animator animator;
public float movespeed;
public SpriteRenderer spriteRenderer;
public GameObject starPrefab;
public Transform starShootPoint;
public new Camera camera;
[SerializeField]private int hp;
public SpriteRenderer hpSpriteRenderer;
public Sprite[] hpSprites;
//CD
private float shootCD = 0.1f;
private float shootCDTimer;
public int Hp
{
get => hp;
set
{
hp = value;
hpSpriteRenderer.sprite = hpSprites[hp];
if (hp == 0)
{
Dead();
}
}
}
private void Awake()
{
Instance = this;
Hp = hpSprites.Length-1;
}
private void Start()
{
spriteRenderer= gameObject.GetComponent<SpriteRenderer>();
}
// Update is called once per frame
void Update()
{
Debug.Log(Hp);
Vector3 mouseWorldPosition=camera.ScreenToWorldPoint(Input.mousePosition);
//Debug.Log(mouseWorldPosition);
Move(mouseWorldPosition); Shoot(mouseWorldPosition);
}
private void Move(Vector3 mouseWorldPosition)
{
animator.GetComponent<Animator>();
float inputX = Input.GetAxis("Horizontal");
float inputY = Input.GetAxis("Vertical");
//修改动画
bool isMove = inputX != 0 || inputY != 0;
animator.SetBool("Move", isMove);
//转向
if (mouseWorldPosition.x > transform.position.x) spriteRenderer.flipX = false;
else if (mouseWorldPosition.x < transform.position.x) spriteRenderer.flipX = true;
//位移
Vector3 pos =
new Vector3(inputX * Time.deltaTime * movespeed,
inputY * Time.deltaTime * movespeed,
0);
transform.Translate(pos, Space.Self);
}
private void Shoot(Vector3 mouseWorldPosition)
{
if (shootCDTimer > 0)
{
shootCDTimer-= Time.deltaTime;
return;
}
if (Input.GetMouseButton(0))
{
shootCDTimer = shootCD;
//实例化
GameObject star=GameObject.Instantiate(starPrefab);
star.transform.position = starShootPoint.position;
Vector3 moveDir = (mouseWorldPosition - transform.position).normalized;
star.GetComponent<Star>().Init(moveDir);
}
}
internal void Hurt()
{
if (Hp <= 0) return;
Hp -= 1;
}
private void Dead()
{
animator.SetTrigger("Dead");
}
private void DestroyPlayer()
{
Destroy(gameObject);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Monster : MonoBehaviour
{
public float moveSpeed;
public int hp;
public Animator animator;
private void Update()
{
if (Game.Instance == null) return;
Vector3 moveDir=Game.Instance.transform.position-transform.position;
transform.Translate(moveSpeed * Time.deltaTime * moveDir,Space.World);
bool isRight=transform.position.x>Game.Instance.transform.position.x;
if (isRight) transform.localScale = new Vector3(-1, 1, 1);
else transform.localScale = new Vector3(1, 1, 1);
}
private void OnTriggerEnter2D(Collider2D collision)
{
Debug.Log("碰撞对象标签:" + collision.gameObject.tag);
if (hp == 0) return;
//接触子弹
if (collision.gameObject.CompareTag("Bullet"))
{
Debug.Log("子弹接触怪物了");
Destroy(collision.gameObject);//取消子弹穿透
hp -= 1;
if (hp == 0)
{
animator.SetTrigger("Death");
}
}
else
{
//接触玩家
if (collision.gameObject.CompareTag("Player"))
{
Debug.Log("11");
collision.gameObject.GetComponent<Game>().Hurt();
Debug.Log("怪物接触玩家了");
}
}
}
#region 动画事件
private void OnDeathAnimationEnd()
{
Destroy(gameObject);
}
#endregion
}