游戏开发实战Unity手游第一人称视角,双摇杆控制,FPS射击游戏Demo(教程 | 含Demo工程源码)
Posted 林新发
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了游戏开发实战Unity手游第一人称视角,双摇杆控制,FPS射击游戏Demo(教程 | 含Demo工程源码)相关的知识,希望对你有一定的参考价值。
文章目录
一、前言
嗨,大家好,我是新发。
有同学私信我,问我能不能写一篇Unity手游第一人称视角控制的教程,
那么,今天就来做个Demo
吧~
注:
Demo
工程源码见文章末尾
最终效果如下:
二、实现方案
1、无主之地,第一人称视角
第一人称视角的游戏大家应该不陌生,比如《无主之地》,
不过它是PC
平台的,使用WASD
控制移动,使用鼠标来控制镜头角度,单击鼠标左键开枪。注:你也可以接手柄来操作~
那么,如果我们想做移动端(手机端)的第一人称视角,如何做角色控制呢?
2、我之前做的摇杆控制
手机端比较常见的就是摇杆控制了,我之前在几篇博客中都有做过摇杆控制,
《【游戏开发创新】用Unity等比例制作广州地铁,广州加油,早日战胜疫情(Unity | 地铁地图 | 第三人称视角)》
《【游戏开发创新】上班通勤时间太长,做一个任意门,告别地铁与塞车(Unity | 建模 | ShaderGraph | 摇杆 | 角色控制)》
《【游戏开发实战】新发教你做游戏(六):教你2个步骤实现摇杆功能》
3、第一人称视角 + 摇杆控制
我上面做的都是第三人称视角的摇杆控制,我们改成第一人称视角即可,也就是第一人称视角+摇杆控制
,像这样子,(图片说明:下图是我在《无主之地》游戏截图中P
了摇杆的UI
)
三、开始实战
1、资源获取:Unity AssetStore
我没有《无主之地》的资源,没关系,我们去Unity
的AssetStore
上找一下FPS
射击游戏的资源,
注:
Unity AssetStore
地址:https://assetstore.unity.com/
关于资源的搜索,我之前写过一篇文章:《Unity游戏开发——新发教你做游戏(二):60个Unity免费资源获取网站》
搜索关键字FPS Pack
,马上就搜到了一个免费的资源Low Poly FPS Pack
,
我们点击添加至我的资源
(注意需要先登录你的Unity
账号),
然后回到Unity
编辑器中,点击菜单Windows / Package Manager
,打开PackageManager
窗口,就可以看到我们刚刚在AssetStore
中添加的资源啦,我们把资源包下载并导入我们的工程即可。
注:你得先创建一个空工程,然后再导入资源包。我之前写过《学Unity的猫》系列教程,其中第三章有讲创建工程的步骤,
《【学Unity的猫】——第三章:第一个Unity工程,你好喵星人》)
2、Low Poly FPS Pack资源运行效果
Low Poly FPS Pack
资源包中已经帮我们做好了一个简单的第一人称FPS
游戏Demo
,我们打开Assault_Rifle_01_Demo
场景,如下
运行,测试效果如下
如你所见,经典的PC
平台FPS
射击游戏玩法,使用WASD
控制移动,使用鼠标来控制镜头角度,单击鼠标左键开枪。
接下来,我们要给它做下手术,改成 摇杆 和 按钮 控制。
3、制作UI界面
3.1、UI素材获取
摇杆图片简单处理,用一个圆就可以了,然后我们还需要一些按钮图标,比如开枪、丢手雷、跳跃、装子弹等,这里推荐我平时经常用的一个查找图标资源的网站,阿里图标库:https://www.iconfont.cn/
比如我搜关键字:枪,就可以看到枪的图标啦~
可以直接免费下载,而且还可以事先修改图片颜色,建议改成白色,这样方便在Unity
中设置其他颜色,
根据你自身的需要下载一些图标资源,我下载的图标如下,
注意,因为我们要在UGUI
中显示这些图标,需要将它们的Texture Type
设置为Sprite (2D and UI)
,如下
3.2、创建UI摄像机:UICamera
建议UI
的显示使用一个单独的摄像机来渲染,我们在场景中创建一个Camera
,重命名为UICamera
,
注:创建摄像机的操作步骤:在
Hierarchy
视图中鼠标右键,然后点击菜单Camera
即可。
设置摄像机的Clear Flags
为Depth only
,设置Culling Mask
只渲染UI
层,设置Projection
为Orthographic
(正交模式),设置Depth
为1
(确保UI
摄像机的比3D
摄像机后渲染),
3.3、创建UI画布:Canvas
接下来,我们在Hierarchy
视图中鼠标右键,点击菜单UI / Canvas
,创建一个Canvas
,
设置一下参数,如下,目的是让UICamera
来渲染Canvas
的内容,并设置分辨率适配规则,
3.4、创建Panel:GamePanel
我们在Canvas
子节点下创建一个Panel
,重命名为GamePanel
,并把Image
组件禁用,
下面我们再在GamePanel
下去创建UI
对象。
3.5、制作摇杆
我们先做移动控制的摇杆,在GamePanel
子节点下创建一个Image
,重命名为moveJointedArm
,
设置左下角对齐,并调整坐标和尺寸,
像这样,它就是我们摇杆的检测区域,
我们把它的Image
组件的颜色的Alpha
通道设置为0
,这样我们就看不见它了,
我们在moveJointedArm
子节点下再创建两个Image
,分别命名为bg
和center
,
分别设置一下尺寸,图片,颜色,如下
效果
同理,做一下右摇杆,
效果
3.6、制作操作按钮
除了移动和旋转,我们还有开枪、丢手雷、跳跃、装子弹的操作,配套需要制作对应的按钮。安排上,
效果
到这里,我们的UI
界面就基本做好啦,下面就是写代码的环节了~
4、摇杆控制脚本:JointedArm.cs
摇杆的逻辑实现,我之前写过一篇文章讲过原理:《Unity使用ScrollRect制作摇杆(UGUI)》,这里我就不过对赘述,直接说下操作流程。
4.1、JointedArm.cs脚本代码
创建一个C#
脚本,重命名为JointedArm.cs
,代码如下:
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using System;
// 摇杆逻辑
public class JointedArm : ScrollRect, IPointerDownHandler
{
public Action<Vector2> onDragCb;
public Action onStopCb;
protected float mRadius = 0f;
private Transform trans;
private RectTransform bgTrans;
private Camera uiCam;
private Vector3 originalPos;
protected override void Awake()
{
base.Awake();
trans = transform;
bgTrans = trans.Find("bg") as RectTransform;
uiCam = GameObject.Find("UICamera").GetComponent<Camera>();
originalPos = trans.localPosition;
}
void Update()
{
if (Input.GetMouseButtonUp(0))
{
//松手时,摇杆复位
trans.localPosition = originalPos;
this.content.localPosition = Vector3.zero;
}
}
protected override void Start()
{
base.Start();
//计算摇杆块的半径
mRadius = bgTrans.sizeDelta.x * 0.5f;
}
public override void OnDrag(PointerEventData eventData)
{
base.OnDrag(eventData);
var contentPostion = this.content.anchoredPosition;
if (contentPostion.magnitude > mRadius)
{
contentPostion = contentPostion.normalized * mRadius;
SetContentAnchoredPosition(contentPostion);
}
//Debug.Log("摇杆滑动,方向:" + contentPostion);
if(null != onDragCb)
onDragCb(contentPostion);
}
public override void OnEndDrag(PointerEventData eventData)
{
base.OnEndDrag(eventData);
//Debug.Log("摇杆拖动结束");
if (null != onStopCb)
onStopCb();
}
public void OnPointerDown(PointerEventData eventData)
{
//点击到摇杆的区域,摇杆移动到点击的位置
trans.position = uiCam.ScreenToWorldPoint(eventData.position);
trans.localPosition = new Vector3(trans.localPosition.x, trans.localPosition.y, 0);
}
}
4.2、挂摇杆脚本,设置成员对象
给moveJointedArm
节点挂JointedArm
脚本,并设置Content
为center
节点,如下,
同理设置右摇杆rotateJointedArm
。到此,我们的摇杆就有交互效果了,如下
5、关联UI交互事件
5.1、定义UI成员:GamePanel.cs
我们创建一个GamePanel.cs
脚本,声明UI
对象,如下
using UnityEngine;
public class GamePanel : MonoBehaviour
{
/// <summary>
/// 移动摇杆
/// </summary>
public JointedArm moveJointedArm;
/// <summary>
/// 旋转摇杆
/// </summary>
public JointedArm rotateJointedArm;
/// <summary>
/// 开枪按钮
/// </summary>
public GameObject fireBtn;
/// <summary>
/// 丢手雷按钮
/// </summary>
public GameObject bombBtn;
/// <summary>
/// 跳跃按钮
/// </summary>
public GameObject jumpBtn;
/// <summary>
/// 装子弹按钮
/// </summary>
public GameObject bulletBtn;
void Start()
{
// TODO 关联UI交互事件
}
}
5.2、设置UI对象
把它挂到GamePanel
节点上,并设置变量对象,如下
5.3、设置摇杆委托
在Start
函数中添加摇杆的委托,如下
// GamePanel.cs
void Start()
{
// 移动控制摇杆
moveJointedArm.onDragCb = (direction) =>
{
// TODO 抛出事件
};
moveJointedArm.onStopCb = () =>
{
// TODO 抛出事件
};
// 旋转控制摇杆
rotateJointedArm.onDragCb = (direction) =>
{
// TODO 抛出事件
};
rotateJointedArm.onStopCb = () =>
{
// TODO 抛出事件
};
// ...
}
我们要抛出一些事件,这里要封装一个事件管理器。
6、事件管理:订阅、注销、抛出
6.1、封装事件管理器:EventDispatcher.cs
我在之前的多篇文章中都有到和用到事件管理器,欢迎阅读我之前写的这些文章,里面都有用到事件管理器,
《【游戏开发框架】自制Unity通用游戏框架UnityXFramework,详细教程(Unity3D技能树 | tolua | 框架 | 热更新)》
《【游戏开发创新】用Unity等比例制作广州地铁,广州加油,早日战胜疫情(Unity | 地铁地图 | 第三人称视角)》
《【游戏开发实战】使用Unity 2019制作仿微信小游戏飞机大战(二):搭建基础游戏框架》
《【游戏开发实战】使用Unity制作水果消消乐游戏教程(三):水果拖动与交换逻辑》
《【游戏开发实战】使用Unity制作像天天酷跑一样的跑酷游戏——第七篇:游戏界面的基础UI》
《【学Unity的猫】第十二章:使用Unity制作背包,皮皮的梦想背包》
EventDispatcher
脚本代码:
using UnityEngine;
using System.Collections.Generic;
public delegate void MyEventHandler(params object[] objs);
/// <summary>
/// 游戏事件管理器
/// </summary>
public class EventDispatcher
{
/// <summary>
/// 注册事件
/// </summary>
/// <param name="evt">事件名</param>
/// <param name="handler">响应函数</param>
public void Regist(string evt, MyEventHandler handler)
{
if (handler == null)
return;
if (listeners.ContainsKey(evt))
{
//这里涉及到Dispath过程中反注册问题,必须使用listeners[type]+=..
listeners[evt] += handler;
}
else
{
listeners.Add(evt, handler);
}
}
/// <summary>
/// 注销事件
/// </summary>
/// <param name="evt">事件名</param>
/// <param name="handler">响应函数</param>
public void UnRegist(string evt, MyEventHandler handler)
{
if (handler == null)
return;
if (listeners.ContainsKey(evt))
{
//这里涉及到Dispath过程中反注册问题,必须使用listeners[type]-=..
listeners[evt] -= handler;
if (listeners[evt] == null)
{
//已经没有监听者了,移除.
listeners.Remove(evt);
}
}
}
/// <summary>
/// 抛出事件
/// </summary>
/// <param name="evt">事件名</param>
/// <param name="objs">参数</param>
public void DispatchEvent(string evt, params object[] objs)
{
try
{
if (listeners.ContainsKey(evt))
{
MyEventHandler handler = listeners[evt];
if (handler != null)
handler(objs);
}
}
catch (System.Exception ex)
{
Debug.LogErrorFormat(szErrorMessage, evt, ex.Message, ex.StackTrace);
}
}
public void ClearEvents(string key)
{
if (listeners.ContainsKey(key))
{
listeners.Remove(key);
}
}
private Dictionary<string, MyEventHandler> listeners = new Dictionary<string, MyEventHandler>();
private readonly string szErrorMessage = "DispatchEvent Error, Event:{0}, Error:{1}, {2}";
private static EventDispatcher s_instance;
public static EventDispatcher instance
{
get
{
if (null == s_instance)
s_instance = new EventDispatcher();
return s_instance;
}
}
}
6.2、定义事件名:EventNameDef.cs
我们再创建一个EventNameDef.cs
脚本,用于定义事件名,如下
/// <summary>
/// 事件名定义
/// </summary>
public class EventNameDef
{
/// <summary>
/// 移动
/// </summary>
public const string MOVE = "MOVE";
/// <summary>
/// 旋转
/// </summary>
public const string ROTATE = "ROTATE";
/// <summary>
/// 开枪
/// </summary>
public const string FIRE = "FIRE";
/// <summary>
/// 丢手榴弹
/// </summary>
public const string BOMB = "BOMB";
/// <summary>
/// 跳跃
/// </summary>
public const string JUMP = "JUMP";
/// <summary>
/// 装子弹
/// </summary>
public const string BULLET = "BULLET";
}
6.3、抛出事件
我们回到GamePanel.cs
脚本,在摇杆的委托中抛出事件,
// GamePanel.cs
void Start()
{
// 移动控制摇杆
moveJointedArm.onDragCb = (direction) =>
{
EventDispatcher.instance.DispatchEvent(EventNameDef.MOVE, new Vector3(direction.x, 0, direction.y).normalized, true);
};
moveJointedArm.onStopCb = () =>
{
EventDispatcher.instance.DispatchEvent(EventNameDef.MOVE, Vector3.zero, false);
};
// 旋转控制摇杆
rotateJointedArm.onDragCb = (direction) =>
{
EventDispatcher.instance.DispatchEvent(EventNameDef.ROTATE, new Vector3(direction.x, 0, direction.y).normalized);
};
rotateJointedArm.onStopCb = () =>
{
EventDispatcher.instance.DispatchEvent(EventNameDef.ROTATE, Vector3.zero);
};
// ...
}
6.4、订阅事件和注销事件
摇杆抛出的事件,最终的响应逻辑就是角色移动和旋转,那么我们就要在原来控制角色移动和旋转的脚本中添加事件订阅。
逻辑在哪里呢?逻辑在FPSControllerLPFP.cs
脚本和AutomaticGunScriptLPFP.cs
脚本中。
画个图,方便大家理解,
我们分别在FPSControllerLPFP.cs
脚本和AutomaticGunScriptLPFP.cs
脚本中添加事件订阅和注销,如下
// FPSControllerLPFP.cs
private void Start()
{
// ...
// 订阅事件
EventDispatcher.instance.Regist(EventNameDef.MOVE, OnEventMove);
EventDispatcher.instance.Regist(EventNameDef.ROTATE, OnEventRotate);
// ...
}
private void OnDestroy()
{
// 注销事件
EventDispatcher.instance.UnRegist(EventNameDef.MOVE, OnEventMove);
EventDispatcher.instance.UnRegist(EventNameDef.ROTATE, OnEventRotate);
// ...
}
// AutomaticGunScriptLPFP.cs
private void Start()
{
// ...
// 订阅事件
EventDispatcher.instance.Regist(EventNameDef.MOVE, OnEventMove)unity 第一人称角色控制器手机虚拟双摇杆