0. 前言 本片文章是对https://www.bilibili.com/video/bv1sY411V7tx视频中瞄准线是如何实现的一个讲解教程,欢迎大家去B站给我一键三连鸭!
这个项目的整个代码和资源已经上传到了GitHub,大家可以去看一下 链接:https://github.com/MycroftCooper/TigerShooting2DGame
1. 效果分析
根据视频中的瞄准线效果,我们可以分析出,它应该具有以下几个功能:
准星会时刻和玩家鼠标保持位置一致
瞄准线的另一头始终要在枪上
当准星放在敌人身上,瞄准线和准星会变红
当开火时准星会有所变化
2. 素材准备 根据以上四个功能,我们就可以知道,其实这个瞄准线的效果应该是分为瞄准线和准星两部分的 所以,我们分别准备好这两部分的素材:
其中Line为瞄准线,而Sight为准星。
2.1 瞄准线 Line 瞄准线的素材有两种:
红线:瞄准敌人时使用
白线:没瞄上敌人时使用
准线是要做成预制体放在游戏里的,所以一张图导入Unity后按照Sprite去切就没问题
因为是像素游戏,所以记得正确设置单位像素数 ,网格类型 和过滤模式 ,设置如下:
在切的时候需要注意的是,瞄准线的长度我们想让它是可变的,而且是从固定的一段开始伸缩。所以在用精灵切片的时候需要做两件事:设置平铺边界 (绿色线)和锚点 (设到左边),如下图所示:
如此一来,瞄准线的素材图就导入成功了,接下来就是:
创建一个预制体
添加一个精灵渲染器
将精灵设为刚刚切好的一个瞄准线的精灵图片
设置绘制模式(只有设置成平铺才能任意伸缩)
设置图层顺序(设成-2就能保证瞄准线总是在图层的最前面了)
如下图所示:
瞄准线的素材就准备完毕啦!
2.2 准星Sight 准星有四种素材:
Sight_Off:没瞄到敌人且没开火的准星
Sight_Off_Fire:没瞄到敌人还开火的准星
Sight_On:瞄到敌人但没开火的准星
Sight_On_Fire:瞄到敌人还开火的准星
准星实际上有两种实现方法:
也做成游戏物体加入场景中
切换游戏光标,让游戏的默认光标变为准星
这里我们采用第二种方法吧,第一种大家肯定是会的。
准星的素材图片最好不要使用精灵,而是有一种专门给准星用的格式:光标(Texture 2D) 这种格式的图片无法切图,所以最好把准星的图片自己提前切好再导入,如下图所示:
这样准星的素材我们也导入完毕了!
2.3 素材管理代码 因为在游戏的过程中,我们会根据开火情况和瞄准情况来反复更换准星和瞄准线,所以可以写一份代码来专门管理这两种瞄准线和四种准星的素材。 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private static class SightResouses { private static List<Sprite> spritesList = new List<Sprite>(Resources.LoadAll<Sprite>("Line" )); private static Dictionary<string , Texture2D> textureList = new Dictionary<string , Texture2D>{ {"Sight_On" , Resources.Load<Texture2D>("Sight_On" )}, {"Sight_On_Fire" , Resources.Load<Texture2D>("Sight_On_Fire" )}, {"Sight_Off" , Resources.Load<Texture2D>("Sight_Off" )}, {"Sight_Off_Fire" , Resources.Load<Texture2D>("Sight_Off_Fire" )}, }; private static Sprite GetResouseSprite (string name ) => spritesList.Find(x => x.name == name); public static Texture2D Sight_On { get => textureList["Sight_On" ]; } public static Texture2D Sight_On_Fire { get => textureList["Sight_On_Fire" ]; } public static Texture2D Sight_Off { get => textureList["Sight_Off" ]; } public static Texture2D Sight_Off_Fire { get => textureList["Sight_Off_Fire" ]; } public static Sprite Sight_On_Line { get => GetResouseSprite("Sight_On_Line" ); } public static Sprite Sight_Off_Line { get => GetResouseSprite("Sight_Off_Line" ); } }
注意: 放在预制体里的瞄准线是 精灵 Sprite 作为光标素材的准星是 2D纹理 Texture2D
这样,通过静态类SightResouses,我们就可以轻松的拿到需要的素材了
3. 功能实现 根据之前的效果分析,我们接下来拆分一下功能:
我们需要知道鼠标当前的位置 让瞄准线伸缩到合适长度 让瞄准线指向光标(准星)
我们需要知道当前是否处于开火状态 并且根据开火状态改变瞄准线与准星的素材资源
我们需要知道当前是否处于瞄准敌人的状态 并且根据瞄准状态改变瞄准线与准星的素材资源
那么我们就一个一个的去实现吧!
3.1 伸缩并指向 要想正确的将瞄准线伸缩指向光标,我们需要一下信息:
光标现在在哪(获取光标屏幕坐标->转换坐标得到世界坐标)
瞄准线现在的原点在哪(瞄准线会挂在枪上,所以直接取transform.position就好)
有了两点就可以确定一条直线了!
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public Vector2 MousePos;public Vector2 SightPos;public Vector2 StartPosOffset;public Vector2 CollisionOffset;private void updateMousePos ( ) { Vector3 screenMousePos = new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0 ); MousePos = Camera.main.ScreenToWorldPoint(screenMousePos); SightPos = MousePos + CollisionOffset; }public float LineWide = 0.15f ;private SpriteRenderer sightLineRenderer;private void updateLine ( ) { Vector2 pos = transform.parent.position + (Vector3)StartPosOffset; float d = Vector2.Distance(pos, SightPos); sightLineRenderer.size = new Vector2(d + 0.2f , LineWide); }
注意:为什么要有两个偏移值? StartPosOffset: 实际上是瞄准线的起点偏移值,瞄准线的父物体是枪,如果不设这个偏移值,瞄准线就会从枪中心而不是枪口延申出来 CollisionOffset: 实际上是瞄准线的终点偏移值,准星的锚点我们希望它在图片正中间,但是当它作为光标使用时,它的锚点在左上角,所以需要进行偏移
现在我们可以做到瞄准线根据鼠标所在的位置来正确伸缩了!
那么,指向怎么实现?这里的代码我放在了瞄准线的父物体,枪的控制代码中了,但也不难,代码如下:
1 2 3 4 5 6 7 8 private void lookAtSight ( ) { Vector2 SightPos = SC.SightPos; transform.LookAt(new Vector3(SightPos.x, SightPos.y)); transform.Rotate(new Vector3(0 , -90 , 0 )); }
注意:为什么要旋转-90度? 因为 transform.LookAt() 本质上是让本物体的Z轴正方向指向目标点 可我们是2D游戏,Z轴正方向会垂直于游戏画面,就看不见了,所以得旋转一下
这样,我们就完成了指向
伸缩和指向两个功能都是实线啦!
3.2 状态检测与素材替换 接下来我们需要实现的是:
瞄准状态的检测:有没有瞄着敌人?
开火状态的检测:有没有在开火?
根据状态进行素材替换
先看开火状态的检测 ,这个简单,看看鼠标的输入就行,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public bool IsOnFire;private void updateOnFire ( ) { if (Input.GetButton("Fire1" )) { IsOnFire = true ; updateSightResouses(); return ; } if (!IsOnFire) return ; IsOnFire = false ; updateSightResouses(); }
注意:为啥是Input.GetButton,不是说要看鼠标的输入吗? 这个啊,这个叫虚拟输入轴,快去我以前的文章学习一下吧:https://mycroftcooper.github.io/2021/03/25/Unity-%E8%BE%93%E5%85%A5%E6%93%8D%E4%BD%9C/
再看瞄准状态的检测 ,这个得用到Physics2D.OverlapCircle,是一种简单的射线检测方式,API如下:Physics2D.OverlapCircle 可以去看看这位老哥写的相关教程:http://www.voycn.com/article/unity2djiancefangfaoverlapcircleyuraycastxiangjie 如果想更加系统化的了解射线检测,可以取看看我之前写的这个文章:https://mycroftcooper.github.io/2021/09/04/Unity-%E5%B0%84%E7%BA%BF%E6%A3%80%E6%B5%8B/
总而言之,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public bool IsOnEntity;public LayerMask EnemiesLayer;public Vector2 CollisionOffset;public float collisionRadius = 0.25f ;private void updateOnEntity ( ) { Collider2D entity = Physics2D.OverlapCircle((Vector2)MousePos + CollisionOffset, collisionRadius, EnemiesLayer); if (entity != null && entity.tag == "Enemies" ) { if (IsOnEntity) return ; IsOnEntity = true ; updateSightResouses(); return ; } if (!IsOnEntity) return ; IsOnEntity = false ; updateSightResouses(); }
如果你想在编辑器中看到你的射线检测范围,可以使用如下代码:
1 2 3 4 private void OnDrawGizmos ( ) { Gizmos.color = debugCollisionColor; Gizmos.DrawWireSphere(SightPos, collisionRadius); }
完成后如下图所示:
绿圈圈就是你的射线检测范围啦(我这里没改偏移量有点偏,你们记得根据自己的精度需要去设置哦)
这样以来,瞄准线状态的检测就算完成啦,接下来进行素材替换的实现,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private void updateSightResouses ( ) { if (IsOnEntity) { sightLineRenderer.sprite = SightResouses.Sight_On_Line; if (IsOnFire) { Cursor.SetCursor(SightResouses.Sight_On_Fire, hotSpot, CursorMode.Auto); } else { Cursor.SetCursor(SightResouses.Sight_On, hotSpot, CursorMode.Auto); } } else { sightLineRenderer.sprite = SightResouses.Sight_Off_Line; if (IsOnFire) { Cursor.SetCursor(SightResouses.Sight_Off_Fire, hotSpot, CursorMode.Auto); } else { Cursor.SetCursor(SightResouses.Sight_Off, hotSpot, CursorMode.Auto); } } }
之前写的素材管理代码用上了吧!
如此一来,整个瞄准线的功能就实现了,可以把它挂到枪上咯!
4. 源代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 using System.Collections.Generic;using UnityEngine;public class SightController : MonoBehaviour { private static class SightResouses { private static List <Sprite > spritesList = new List<Sprite>(Resources.LoadAll<Sprite>("Line" )); private static Dictionary<string , Texture2D> textureList = new Dictionary<string , Texture2D>{ {"Sight_On" , Resources.Load<Texture2D>("Sight_On" )}, {"Sight_On_Fire" , Resources.Load<Texture2D>("Sight_On_Fire" )}, {"Sight_Off" , Resources.Load<Texture2D>("Sight_Off" )}, {"Sight_Off_Fire" , Resources.Load<Texture2D>("Sight_Off_Fire" )}, }; private static Sprite GetResouseSprite (string name ) => spritesList.Find(x => x.name == name); public static Texture2D Sight_On { get => textureList["Sight_On" ]; } public static Texture2D Sight_On_Fire { get => textureList["Sight_On_Fire" ]; } public static Texture2D Sight_Off { get => textureList["Sight_Off" ]; } public static Texture2D Sight_Off_Fire { get => textureList["Sight_Off_Fire" ]; } public static Sprite Sight_On_Line { get => GetResouseSprite("Sight_On_Line" ); } public static Sprite Sight_Off_Line { get => GetResouseSprite("Sight_Off_Line" ); } } public Vector2 MousePos; public Vector2 SightPos; public Vector2 StartPosOffset; void Start ( ) { IsOnFire = false ; IsOnEntity = false ; sightLineRenderer = GetComponentInChildren<SpriteRenderer>(); sightLineRenderer.sprite = SightResouses.Sight_Off_Line; Cursor.SetCursor(SightResouses.Sight_Off, hotSpot, CursorMode.Auto); } void Update ( ) { updateSight(); updateLine(); } #region 准星相关 [Header("Sight" ) ] public Vector2 hotSpot; public bool IsOnFire; public bool IsOnEntity; [Header("Collision" ) ] public LayerMask EnemiesLayer; public Vector2 CollisionOffset; public float collisionRadius = 0.25f ; private Color debugCollisionColor = Color.green; private void updateSight ( ) { updateMousePos(); updateOnEntity(); updateOnFire(); } private void updateMousePos ( ) { Vector3 screenMousePos = new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0 ); MousePos = Camera.main.ScreenToWorldPoint(screenMousePos); SightPos = MousePos + CollisionOffset; } private void updateSightResouses ( ) { if (IsOnEntity) { sightLineRenderer.sprite = SightResouses.Sight_On_Line; if (IsOnFire) { Cursor.SetCursor(SightResouses.Sight_On_Fire, hotSpot, CursorMode.Auto); } else { Cursor.SetCursor(SightResouses.Sight_On, hotSpot, CursorMode.Auto); } } else { sightLineRenderer.sprite = SightResouses.Sight_Off_Line; if (IsOnFire) { Cursor.SetCursor(SightResouses.Sight_Off_Fire, hotSpot, CursorMode.Auto); } else { Cursor.SetCursor(SightResouses.Sight_Off, hotSpot, CursorMode.Auto); } } } private void updateOnFire ( ) { if (Input.GetButton("Fire1" )) { if (IsOnFire) return ; IsOnFire = true ; updateSightResouses(); return ; } if (!IsOnFire) return ; IsOnFire = false ; updateSightResouses(); } private void updateOnEntity ( ) { Collider2D entity = Physics2D.OverlapCircle((Vector2)MousePos + CollisionOffset, collisionRadius, EnemiesLayer); if (entity != null && entity.tag == "Enemies" ) { if (IsOnEntity) return ; IsOnEntity = true ; updateSightResouses(); return ; } if (!IsOnEntity) return ; IsOnEntity = false ; updateSightResouses(); } private void OnDrawGizmos ( ) { Gizmos.color = debugCollisionColor; Gizmos.DrawWireSphere(SightPos, collisionRadius); } #endregion #region 准线相关 public float LineWide = 0.15f ; private SpriteRenderer sightLineRenderer; private void updateLine ( ) { Vector2 pos = transform.parent.position + (Vector3)StartPosOffset; float d = Vector2.Distance(pos, SightPos); sightLineRenderer.size = new Vector2(d + 0.2f , LineWide); } #endregion }
别忘了父物体枪械上的代码:
1 2 3 4 5 6 7 8 private void lookAtSight() { Vector2 SightPos = SC.SightPos; transform.LookAt(new Vector3(SightPos.x , SightPos.y ) ); transform.Rotate(new Vector3(0, -90, 0) ); }
当然想放在瞄准线的控制代码里也行,如果不需要枪械也同步旋转的话。
5. 后语 这个小项目还有其它很多小技术点,感觉后面可以再跟大家分享一下!你们有说明想要交流的也可以直接跟我留言鸭! B站账号:https://space.bilibili.com/172549987?spm_id_from=333.1007.0.0 CSDN账号:https://blog.csdn.net/qq_44705559?spm=1001.2101.3001.5343 GitHub账号:https://github.com/MycroftCooper 个人博客:https://mycroftcooper.github.io/