【Unity学习心得】如何制作俯视角射击游戏

news/2024/9/18 12:29:12/ 标签: 游戏, c#, unity, 游戏引擎

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、导入素材
  • 二、制作流程
    • 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判断是否存在对应的键值或者队列的内容数量为0if (!objectPool.ContainsKey(prefab.name) || objectPool[prefab.name].Count == 0){
//如果没有就新建从Object Pool -> Child Pool -> PrefabgameObject = 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的透明度来渐变颜色淡出直到0while (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 / 2fbullet.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);}}
}

给这两个武器添加上去(其它三个是下一期要讲的先别在意):


总结

最后的效果如图所示:


http://www.ppmy.cn/news/1525426.html

相关文章

EmguCV学习笔记 C# 11.6 图像分割

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 EmguCV是一个基于OpenCV的开源免费的跨平台计算机视觉库,它向C#和VB.NET开发者提供了OpenCV库的大部分功能。 教程VB.net版本请访问…

ctfshow--信息收集题目全解

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文记录ctfshow信息收集部分打靶记录 web1 这题弱智&#xff0c;f12进入查看源码&#xff0c;flag在注释里。 (这告诉我们&#xff0c;开发者的注释我们也是可以看到的&#xff0c;所以版权&#xff0c;源码地址&…

个人随想:嵌入学习桌的智能学习与陪伴助手

随着大模型技术的快速发展&#xff0c;我们对于7B、70B、80B甚至405B等开源大模型已经不陌生。在有GPU支持的情况下&#xff0c;许多人会倾向选择更大参数的模型&#xff0c;因为通常参数越大&#xff0c;效果越好&#xff0c;这已成为行业共识。 . 然而&#xff0c;随着量化技…

.NET 一款在线解密Web.config的脚本

01阅读须知 此文所提供的信息只为网络安全人员对自己所负责的网站、服务器等&#xff08;包括但不限于&#xff09;进行检测或维护参考&#xff0c;未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的直接或间接后果和损失&#xf…

man命令详解

一、man命令简介&#xff1a; man是manual的缩写。操作手册之意。 本地的帮助文档称为man pages&#xff0c;这些操作手册随着软件安装而安装到本地&#xff0c;可以使用man命令进行查询。 随着软件包的安装有些操作手册会以文档的方式放在/usr/share/doc目录当中。…

网络设备安全

网络设备安全概况 交换机安全威胁&#xff1a;交换机是网络基础设备&#xff0c;负责网络通信数据包的交换传输 交换机面临的网络安全威胁&#xff1a; 路由器安全威胁 网络设备安全机制与实现技术 认证机制&#xff1a;为防止网络设备滥用&#xff0c;网络设备读用户身份进行…

c# socket通信实例

服务器端 主要代码 //服务器socket private Socket serverSocket; private void InitServerSocket() {//创建终结点&#xff08;EndPoint&#xff09;IPAddress ip IPAddress.Any;IPEndPoint ipe new IPEndPoint(ip, 10030);//创建 socket 并开始监听serverSocket new Soc…

PyQT开发总结

用PyQT开发了一个界面小程序&#xff0c;记录一下。 pyuic和pyrcc pyuic &#xff08;PYthon User Interface Compiler&#xff09;是一个命令行工具&#xff0c;用于将 Qt Designer 生成的 .ui 文件转换成 Python 代码。pyrcc 用于处理 Qt 资源文件&#xff08;如图片&#…

海外云手机有哪些推荐?

随着云手机的发展&#xff0c;越来越多的企业和个人开始使用云手机来满足他们的海外业务需求。用户可以通过云手机实现方便、快捷的海外访问&#xff0c;一般用来进行tiktok运营、亚马逊电商运营、海外社媒运营等操作。海外云手机平台有很多&#xff0c;以下是一些比较好的云手…

波场TRON领航者孙宇晨:区块链行业的青年先锋与标杆

​孙宇晨&#xff0c;作为波场TRON的创始人&#xff0c;是区块链行业中备受瞩目的青年榜样。他的成长历程和对区块链技术的贡献&#xff0c;展现了他在这个快速发展的领域中的领导力和远见。 孙宇晨的区块链旅程始于他对去中心化技术的强烈兴趣和信念。他早年在北京大学和宾夕法…

【系统架构设计师-2013年真题】案例分析-答案及详解

更多内容请见: 备考系统架构设计师-核心总结索引 文章目录 【材料1】问题1问题2【材料2】问题1问题2问题3问题4【材料3】问题1问题2问题3【材料4】问题1问题2问题3【材料5】问题1问题2问题3【材料1】 阅读以下关于企业应用系统集成架构设计的说明,在答题纸上回答问题1和问题…

为什么在 JSON 序列化中不使用 transient

有些小伙伴发现了&#xff0c;明明在返回的实体类中指定了属性为transient。为什么前端得到的返回json中还是有这个属性的值&#xff1f; 类&#xff1a; private String name; private transient String password;返回结果&#xff1a; { name:"刘大大", password:…

MyBatis-PlusDruid数据源

MyBatis-Plus简介 MyBatis-Plus&#xff08;简称MP&#xff09;是一个MyBatis的增强工具&#xff0c;它在MyBatis的基础上进行了增强而不改变其原有的功能&#xff0c;旨在简化开发、提高效率。以下是对MyBatis-Plus的详细简介&#xff1a; 一、基本概述 定义&#xff1a;MyBat…

主流无线物联网通信技术有哪些

主流无线物联网通信技术主要包括以下几种&#xff0c;以下是对它们的详细分析和介绍&#xff1a; 一、短距离无线通信技术 蓝牙&#xff08;Bluetooth&#xff09; 蓝牙是一种大容量近距离无线数字通信技术标准&#xff0c;旨在实现最高数据传输速率1Mbps、最大传输距离为10…

高效执行自动化用例:分布式执行工具pytest-xdist实战

01声明 在介绍pytest-xdist时&#xff0c;不讲任何原理&#xff0c;需要看原理的请移至官方&#xff1a;pytest-xdist PyPI 当我们自动化测试用例非常多的时候&#xff0c; 一条条按顺序执行会非常慢&#xff0c;pytest-xdist的出现就是为了让自动化测试用例可以分布式执行&am…

快速入门游戏领域,开发游戏需要哪些技术?

在这个充满创意和技术的时代&#xff0c;游戏行业成为众多创新人才追求梦想的热土。对于准备踏入这个充满挑战与机遇的领域的新人来说&#xff0c;了解游戏开发流程是至关重要的。 游戏市场蓬勃发展&#xff0c;游戏行业未来行情可观&#xff0c;在这个充满创意和技术的时代&a…

OpenCV进行灰度变换

在Python中,处理图像(包括灰度变换)通常通过第三方库来完成,比如OpenCV和Pillow(PIL的更新版)。这里将分别介绍如何使用这两个库来进行灰度变换。 使用OpenCV进行灰度变换 OpenCV是一个开源的计算机视觉和机器学习软件库,它提供了大量图像处理的功能。使用OpenCV将图像…

git分支合并

git分支合并 说明步骤 说明 假如目前有两个分支 : main分支 和 master分支,你要将main分支合并到master分支,现在你正处于main分支. $ git branch * mainmaster步骤 切换到 master 分支&#xff1a; 首先&#xff0c;你需要切换到 master 分支&#xff0c;因为合并操作是在目…

Python “集合” 100道实战题目练习,巩固知识、检查技术

本文主要是作为Python中列表的一些题目&#xff0c;方便学习完Python的集合之后进行一些知识检验&#xff0c;感兴趣的小伙伴可以试一试&#xff0c;含选择题、判断题、实战题、填空题&#xff0c;答案在第五章。 在做题之前可以先学习或者温习一下Python的列表&#xff0c;推荐…

如何用Docker运行Django项目

本章教程,介绍如何用Docker创建一个Django,并运行能够访问。 一、拉取镜像 这里我们使用python3.11版本的docker镜像 docker pull python:3.11二、运行容器 这里我们将容器内部的8080端口,映射到宿主机的80端口上。 docker run -itd --name python311 -p