提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、导入素材
- 二、制作流程
- 1.制作地图
- 2.实现人物动画和移动脚本
- 3.制作单例模式和对象池
- 4.制作手枪pistol和子弹bullet和子弹壳bulletShell
- 5.制作散弹枪shotgun
- 总结
前言
俯视角射击游戏类似元气骑士那种,懂的都懂好吧。
本节课我们将要实现:制作地图,实现人物动画和移动脚本,制作单例模式和对象池,制作手枪pistol和子弹bullet和子弹壳bulletShell,制作散弹枪shotgun。
一、导入素材
素材链接: 、
https://o-lobster.itch.io/simple-dungeon-crawler-16x16-pixel-pack https://humanisred.itch.io/weapons-and-bullets-pixel-art-asset
二、制作流程
1.制作地图
还是要用我们的老朋友Tilemap来做这种像素地图:
可以看到,我们创建了三个层级的Grid,记得在Sorting Layer分别给它们排好选择顺序,除了Ground的那一层以外其它记得要给 tilemap Collider2D和Composite Collider2D,Rigibody2D记得设置成静态,这些后面都要用到的。
2.实现人物动画和移动脚本
绘制完简单地图后,我们就要开始导入人物素材了。公式化三件套:Sprite Renderer,Rb2D设置为Kinematic被动检测碰撞,别忘了锁Z轴旋转,Animator自己根据素材创建Idle,Walk
接下来创建PlayerController.cs给Player对象
代码如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : SingletonMono<PlayerController>
{
public float speed = 3f;
public bool enabledInput = true;
private Rigidbody2D rb2d;
private Animator anim;
private Camera mainCamera;
private Vector2 input;
private Vector2 mousePosition;
private new void Awake()
{
rb2d = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
mainCamera = Camera.main;
}
void Update()
{
if (enabledInput)
{
mousePosition = mainCamera.ScreenToWorldPoint(Input.mousePosition);
input.x = Input.GetAxisRaw("Horizontal");
input.y = Input.GetAxisRaw("Vertical");
rb2d.velocity = input.normalized * speed;
if(input != Vector2.zero)
{
anim.SetBool("isMoving", true);
}
else
{
anim.SetBool("isMoving", false);
}
if(mousePosition.x >= transform.position.x)
{
transform.rotation = Quaternion.Euler(new Vector3(0f, 0f, 0f));
}
else
{
transform.rotation = Quaternion.Euler(new Vector3(0f, 180f, 0f));
}
}
}
}
这些大伙都应该懂了就获取X和Y上的Input,方向化后经典给rb2d设置velocity,然后根据鼠标位置判断玩家是否要沿Y轴翻转。
顺便给camera搞个cinemachine,让相机跟随玩家移动:
至此,我们实现了实现人物动画和移动脚本,接下来该开始制作单例模式和对象池模式了。
3.实现单例模式和对象池模式
单例模式大伙肯定也懂的,但这几天重温C#知识我突然就想用泛型<T>来做个泛型单例类,让所有MONOBehaviour的类都能继承:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour
{
public static T _instance;
public static T Instance
{
get
{
if(_instance == null)
{
GameObject gameObject = new GameObject();
_instance = gameObject.AddComponent<T>();
DontDestroyOnLoad(gameObject);
}
return _instance;
}
}
public void Awake()
{
if (_instance == null)
{
_instance = (gameObject as T);
DontDestroyOnLoad(gameObject);
return;
}
if (this != _instance)
{
DestroyImmediate(gameObject);
return;
}
}
}
单例模式随便完成咯,接下来开始做对象池模式,很多人对对象池的编写还是比较陌生的,这里先写出主要思想:
在Unity中,实时创建(GameObject.Instantiate())和销毁游戏对象(GameObject.Destory ())会造成相当大的开销。
对于一些简单的,可以复用的物体,我们可以考虑用Enable/Disable来代替创建与销毁,这是因为Enable/Disable对性能的消耗搞更小。
我们可以采用对象池的思想实现这个功能。
所谓对象池,就是把所有相同的对象放在一个池子中,每当要使用到一个对象时,就从池子中找找看有没有之前创建过但现在空闲的物体可以直接拿来用,如果没有的话我们再创建物体并扔进池子里。
想要销毁一个物体,我们只需要标记其“空闲”即可,并不会直接销毁它。
理解过后我们就可以编写一个脚本ObjectPool.cs
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool : SingletonMono<ObjectPool>
{
private Dictionary<string, Queue<GameObject>> objectPool = new Dictionary<string, Queue<GameObject>>();
private GameObject pool;
private new void Awake()
{
objectPool = new Dictionary<string, Queue<GameObject>>();
}
//从对象池中获取对象
public GameObject GetObject(GameObject prefab)
{
GameObject gameObject;
//先从name判断是否存在对应的键值或者队列的内容数量为0
if (!objectPool.ContainsKey(prefab.name) || objectPool[prefab.name].Count == 0)
{
//如果没有就新建从Object Pool -> Child Pool -> Prefab
gameObject = GameObject.Instantiate(prefab);
PushObject(gameObject);
if (pool == null)
pool = new GameObject("ObjectPool");
GameObject childPool = GameObject.Find(prefab.name + "Pool");
if (!childPool)
{
childPool = new GameObject(prefab.name + "Pool");
childPool.transform.SetParent(pool.transform);
}
gameObject.transform.SetParent(childPool.transform);
}
gameObject = objectPool[prefab.name].Dequeue();
gameObject.SetActive(true); //可视化
return gameObject;
}
//从对象池中取出对象
public void PushObject(GameObject prefab)
{
//要保证名字和objectPool的名字相等,因此我们要用空的字符串取代Unity新建游戏对象会有个"(Clone)"
string name = prefab.name.Replace("(Clone)", string.Empty);
if (!objectPool.ContainsKey(name))
objectPool.Add(name, new Queue<GameObject>());
objectPool[name].Enqueue(prefab); //创建该prefab名字的队列并让prefab入栈
prefab.SetActive(false);//默认为不可见
}
}
4.制作手枪pistol和子弹bullet和子弹壳bulletShell
终于来到了重点制作这些东西,当一些事物存在共性的时候我们会想使用抽象类来减少代码的耦合度,同样这些手枪火箭筒啥的都输入枪,我们可以创建一个Gun的Prefab,让这些枪都成为Gun的Varient。
其中muzzle是子弹发射位置,bulletshell是生成弹壳的位置。
我们Varient第一个目标是pistol手枪,如图我们给它sprite和animator
制作两个动画并连接即可
别忘了动态调整muzzle和bulletshell的位置
回到脚本当中,新建一个基类脚本Gun.cs,我们打算让所有的枪械类都继承这个脚本:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Gun : MonoBehaviour
{
public GameObject bulletPrefab;
public GameObject bulletShellPrefab;
public float interval = 0.5f;
protected Animator animator;
protected Camera mainCamera;
protected Transform muzzleTrans;
protected Transform bulletShellTrans;
protected float flipY;
protected Vector2 mousePos;
protected Vector2 direction;
private float timer;
protected virtual void Start()
{
animator = GetComponent<Animator>();
mainCamera = Camera.main;
muzzleTrans = transform.Find("Muzzle"); ;
bulletShellTrans = transform.Find("BulletShell");
flipY = transform.localScale.y;
}
protected virtual void Update()
{
mousePos = mainCamera.ScreenToWorldPoint(Input.mousePosition);
if(mousePos.x >= transform.position.x)
{
transform.localScale = new Vector3(transform.localScale.x, flipY, 1);
}
else
{
transform.localScale = new Vector3(transform.localScale.x, -flipY, 1);
}
Shoot();
}
//控制枪械发射间隔
protected virtual void Shoot()
{
direction = (mousePos - new Vector2(transform.position.x, transform.position.y)).normalized;
transform.right = direction;
if (timer != 0)
{
timer -= Time.deltaTime;
if (timer < 0)
timer = 0;
}
if (Input.GetKeyDown(KeyCode.Mouse0)) //按下鼠标左键
{
if(timer == 0)
{
timer = interval;
Fire();
}
}
}
//控制开火
protected virtual void Fire()
{
animator.SetTrigger("Shoot");
//生成子弹预制体
GameObject bullet = ObjectPool.Instance.GetObject(bulletPrefab);
bullet.transform.position = muzzleTrans.position;
//发射子弹角度偏差
float angle = UnityEngine.Random.Range(-5f, 5f);
bullet.GetComponent<Bullet>().SetSpeed(Quaternion.AngleAxis(angle , Vector3.forward) * direction);
//生成子弹壳预制体
GameObject bulletShell = ObjectPool.Instance.GetObject(bulletShellPrefab);
bulletShell.transform.position = bulletShellTrans.position;
bulletShell.transform.rotation = bulletShellTrans.rotation;
}
}
然后我们再给pistol添加同名脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Pistol : Gun
{
}
写到这里我们注意到还要接着制作子弹Bullet和子弹壳BulletShell的预制体:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
public float bulletSpeed = 15f;
public GameObject explosionPrefab;
private Rigidbody2D rb2d;
private void Awake()
{
rb2d = GetComponent<Rigidbody2D>();
}
//在这里,我们用通过给子弹的rb2d设置速度velocity控制其移动速度和方向
public void SetSpeed(Vector2 direction)
{
rb2d.velocity = bulletSpeed * direction;
}
//最后当子弹碰到墙壁时对象池回收该对象并生成爆炸对象
private void OnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.layer == LayerMask.NameToLayer("Wall"))
{
GameObject exp = ObjectPool.Instance.GetObject(explosionPrefab);
exp.transform.position = transform.position;
ObjectPool.Instance.PushObject(gameObject);
}
}
}
新建一个bullet的prefab
新建一个Explosion的prefab
给它制作一个爆炸的动画
在它的同名脚本中回收爆炸游戏对象:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Explosion : MonoBehaviour
{
private Animator anim;
private AnimatorStateInfo info;
private void OnEnable()
{
anim = GetComponent<Animator>();
}
private void Update()
{
info = anim.GetCurrentAnimatorStateInfo(0);
if(info.normalizedTime >= 1)
{
ObjectPool.Instance.PushObject(gameObject);
}
}
}
同样的操作也给bulletshell
同名脚本中:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BulletShell : MonoBehaviour
{
public float speed;
public float stopTime = 0.5f;
public float fadeSpeed = 0.01f;
private Rigidbody2D rb2d;
private SpriteRenderer sprite;
void Awake()
{
rb2d = GetComponent<Rigidbody2D>();
sprite = GetComponent<SpriteRenderer>();
}
private void OnEnable()
{
float angel = Random.Range(-30f, 30f);
rb2d.velocity = Quaternion.AngleAxis(angel, Vector3.forward) * Vector3.up * speed;
sprite.color = new Color(sprite.color.r, sprite.color.g, sprite.color.b, 1);
rb2d.gravityScale = 3;
StartCoroutine(Stop());
}
private IEnumerator Stop()
{
yield return new WaitForSeconds(stopTime);
rb2d.velocity = Vector2.zero;
rb2d.gravityScale = 0;
//通过spriterenderer的透明度来渐变颜色淡出直到0
while (sprite.color.a > 0)
{
sprite.color = new Color(sprite.color.r, sprite.color.g, sprite.color.g, sprite.color.a - fadeSpeed);
yield return new WaitForFixedUpdate();
}
//然后回收该游戏对象
ObjectPool.Instance.PushObject(gameObject);
}
}
5.制作散弹枪shotgun
有了前车之鉴,我们就可以照着葫芦画瓢。还是老配方先生成varient:
别忘了调整muzzle和bulletshell的位置。
创建同名脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ShotGun : Gun
{
public int bulletNum = 3;
public float bulletAngle = 15f;
//我们只需要重写fire脚本
protected override void Fire()
{
animator.SetTrigger("Shoot");
int med = bulletNum / 2;
for (int i = 0; i < bulletNum; i++)
{
GameObject bullet = ObjectPool.Instance.GetObject(bulletPrefab);
bullet.transform.position = muzzleTrans.position;
if (bulletNum % 2 == 1)
{
//这段代码的意思是如果有奇数个bulletnum那给这个bulletshell设置的角度应该是bulletAngle * (i - //med)
bullet.GetComponent<Bullet>().SetSpeed(Quaternion.AngleAxis(bulletAngle * (i - med), Vector3.forward) * direction);
}
else if(bulletNum % 2 == 0)
{
//这段代码的意思是如果有偶数个bulletnum那给这个bulletshell设置的角度应该是bulletAngle * (i - //med) + bulletAngle / 2f
bullet.GetComponent<Bullet>().SetSpeed(Quaternion.AngleAxis(bulletAngle * (i - med) + bulletAngle / 2f, Vector3.forward) * direction);
}
}
GameObject shell = ObjectPool.Instance.GetObject(bulletShellPrefab);
shell.transform.position = bulletShellTrans.transform.position;
shell.transform.rotation = bulletShellTrans.transform.rotation;
}
}
可以看到奇数个子弹:
偶数个子弹:你看是不是还要再加偏转角的一半即bulletAngle / 2
最后我们还想要根据键盘的Q和E键切换武器,回到playerController.cs当中:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : SingletonMono<PlayerController>
{
public float speed = 3f;
public bool enabledInput = true;
public GameObject[] guns;
private int currentGun;
private Rigidbody2D rb2d;
private Animator anim;
private Camera mainCamera;
private Vector2 input;
private Vector2 mousePosition;
private new void Awake()
{
rb2d = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
mainCamera = Camera.main;
guns[0].SetActive(true);
}
void Update()
{
if (enabledInput)
{
SwitchGun();
mousePosition = mainCamera.ScreenToWorldPoint(Input.mousePosition);
input.x = Input.GetAxisRaw("Horizontal");
input.y = Input.GetAxisRaw("Vertical");
rb2d.velocity = input.normalized * speed;
if(input != Vector2.zero)
{
anim.SetBool("isMoving", true);
}
else
{
anim.SetBool("isMoving", false);
}
if(mousePosition.x >= transform.position.x)
{
transform.rotation = Quaternion.Euler(new Vector3(0f, 0f, 0f));
}
else
{
transform.rotation = Quaternion.Euler(new Vector3(0f, 180f, 0f));
}
}
}
//切换武器
private void SwitchGun()
{
if (Input.GetKeyDown(KeyCode.Q))
{
guns[currentGun].SetActive(false);
if(--currentGun < 0)
{
currentGun = guns.Length - 1;
}
guns[currentGun].SetActive(true);
}
else if (Input.GetKeyDown(KeyCode.E))
{
guns[currentGun].SetActive(false);
if (++currentGun > guns.Length - 1)
{
currentGun = 0;
}
guns[currentGun].SetActive(true);
}
}
}
给这两个武器添加上去(其它三个是下一期要讲的先别在意):
总结
最后的效果如图所示: