文章目录
一、前言
嗨,大家好,我是新发。
现在每天上班的通勤时间是一个多小时,加上下班的通勤时间,每天在路上就是两个半小时,在广州早高峰坐地铁简直要命,这里我不得不吐槽一下广州21
号线,人流量超多,发车频率还低,导致每趟都堆积特别多人,每次都要等至少两三趟才能挤上,而且都好暴力,太疯狂了,这样真的容易出事,每次出地铁心里都在重复一句话:下次不搭21
号线了!
还好有热心同事经常开车搭我上下班,老麻烦别人也很不好意思,不过,搭了几次车又感觉脸皮厚了-_-
要是有一个任意门可以连接家门口和公司门口就好了,现实中没有,那就在虚拟世界里做一个吧~
二、最终效果
我做的Demo
最终效果如下,
家门口:
从家门口穿过传送门:
从公司经过传送门回家:
下面,我就来讲讲我的制作过程吧~
三、Blender建模
看过我前面两篇文章的同学应该知道,我最近自学了Blender
建模,感兴趣的同学可以看下我之前两篇文章,
【游戏开发创新】当我学了Blender 建模,自制3D电脑桌面,回收站爆发了,把我做的模型都吐了出来(Blender | Unity | FBX),
【游戏开发创新】自学Blender建模,自制孔明灯,在Unity中点亮整个星空,愿新年,胜旧年(Unity | 建模 | 粒子系统 | 预设)
1、Blender下载安装
Blender
官网:https://www.blender.org/
Blender
中国社区:https://www.blendercn.org/
Blender
中文手册:https://docs.blender.org/manual/zh-hans/2.79/about/introduction.html
我使用的Blender
版本是2.93.4
,
注:关于
Blender
的教程网上蛮多的,这里我就不过多讲了,掌握基本操作和快捷键,很快就可以上手建模啦~
2、传送门建模
传送门最终模型如下:
3、房子建模
房子最终模型如下:
4、公司大楼建模
公司大楼最终模型如下:
5、文字模型
再做一些文字模型,
四、Blender导出FBX
在Blender
中点击菜单File / Export / FBX
,将模型导出成FBX
格式,
如下:
五、FBX导入Unity中
将FBX
文件导入到Unity
工程中,
把模型放入场景中,现在都是默认的材质,所以都是灰白色的,不着急,下面我们就来做材质~
六、制作材质
1、传送门
材质球使用的shader
我打算使用ShaderGraph
来制作,我之前写过一篇ShaderGraph的文章:《ShaderGraph使用教程与各种特效案例:Unity2020》,推荐先看下这篇文章。
1.1、ShaderGraph准备
安装Universal RP
插件,
在Project
视图中右键鼠标,点击菜单Create / Rendering / Universal Render Pipeline / Pipeline Asset (Forward Renderer)
,
创建UniversalRenderPipelineAsset
,如下
点击菜单Edit / Project Settings...
,打开Project Settings
窗口,选择Graphics
分页,把UniversalRenderPipelineAsse
t拖到Scriptable Render Pipeline Settings
中,
1.2、创建ShaderGraph
在Project
视图中右键鼠标,点击菜单Create / Shader / Universal Render Pipeline / Lit Shader Graph
,创建一个PBR
的ShaderGraph
,
重命名为PortalCenter
,作为传送门中心的shader
,
1.3、编辑ShaderGraph
双击PortalCenter
打开编辑器,编辑节点如下,核心就是对泰森多边形(Voronio
)进行UV旋涡旋转(Twirl
)。
1.4、材质球使用ShaderGraph
创建一个材质球Material
, 重命名为PortalCenter
,
设置材质球的shader
为刚刚的ShaderGraph
文件,
1.5、模型引用材质球
将材质球赋值给传送门模型,
效果如下,
2、房子材质球
同理,制作房子的材质,
效果如下,
3、公司大楼材质球
制作房子的材质,
效果如下,
七、地面
1、广州地铁图
找一张广州最新的地铁地图,我找到的是下面这张,
2、地面(Plane)
在场景中创建一个Plane
平面,制作材质并引用这张图片,
效果如下:
八、主角
1、主角资源
主角我在AssetStore
上找到了一个心仪的模型,推荐给大家,
AssetStore
地址:https://assetstore.unity.com/packages/3d/characters/humanoids/sci-fi/stylized-astronaut-114298
将模型下载导入Unity
中,
2、双摇杆制作
主角的移动和摄像头的角度旋转我想通过摇杆来控制,我们做一个双摇杆功能。
在Canvas
节点上右键点击菜单UI / Panel
,创建一个Panel
,
把Image
组件禁用掉,因为我们不需要Panel
显示出来,
在Panel下
创建一个Image
,重命名为leftJointedArm
,作为左摇杆的父节点,
设置它的锚点为bottom - left
,即屏幕左下角,调整坐标和宽高,
像这样子,
把它的Color
的alpha
调为0
,因为我们只需要利用它的区域来检测触碰,我们不需要肉眼看见它,
接着在它的子节点下创建两个Image
,分别命名为bg
和center
,
它们的Source Image
都设置为摇杆的图片资源,
分别调整下bg
和center
的大小和颜色透明度,效果如下:
同理再做一个右摇杆,
效果如下:
接下来需要给摇杆加上逻辑,Unity
的UGUI
提供了ScrollRect
组件,非常适合用来制作摇杆,我们继承ScrollRect
然后实现OnDrag
和OnEndDrag
方法,可以很方便地获取到摇杆的遥控数据,另外,为了检测区域点击,我们再实现IPointerDownHandler
接口。
创建摇杆脚本JointedArm.cs
,代码如下:
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using System;
/// <summary>
/// 摇杆
/// </summary>
public class JointedArm : ScrollRect, IPointerDownHandler
{
public Action<Vector2> onDragCb;
public Action onStopCb;
protected float radius = 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();
//计算摇杆块的半径
radius = bgTrans.sizeDelta.x * 0.5f;
}
public override void OnDrag(PointerEventData eventData)
{
base.OnDrag(eventData);
var contentPostion = this.content.anchoredPosition;
if (contentPostion.magnitude > radius)
{
contentPostion = contentPostion.normalized * radius;
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);
}
}
把JointedArm.cs
分别挂到leftJointedArm
和rightJointedArm
上,赋值对应的center
,
运行Unity
,摇杆测试效果如下:
接下来我们要实现左摇杆控制主角移动并播放跑的动画,右摇杆控制摄像机角度旋转。
3、主角移动控制
3.1、动画控制
注:关于Animator组件的详细使用可以参见我之前写的这篇文章:《Unity动画状态机Animator使用》
打开角色的动画控制器文件CharacterController
,
可以看到,两个动作,一个idle
(站立)一个Run
(跑),
到Parameters
(参数)里面有一个AnimationPar
参数,这个参数就是用来控制站立与跑着两个动画的过渡条件的,
从Idle
过渡到Run
的条件是AnimationPar
等于1
,
从Run
过渡到Idle
的条件是AnimationPar
等于0
,
这样,我们就可以在代码中通过这个参数来控制动画的过渡了,例:
// public Animator anim;
// 站立 -> 跑
anim.SetInteger("AnimationPar", 1);
// 跑 -> 站立
anim.SetInteger("AnimationPar", 0);
3.2、移动控制
主角的移动控制包括坐标和角度的变化,当摇杆向左滑,主角向左移动,同时主角的朝向也跟着转向左边。
移动我们可以设置transfrom
的position
属性来实现,转向我们可以设置transfrom
的forward
属性来实现,例:
// Vector3 moveDirection; 摇杆向量
// float speed; 移动速度
// float turnSpeed; 转向速度
transform.position += moveDirection * speed * Time.deltaTime;
transform.forward = Vector3.Lerp(transform.forward, moveDirection, turnSpeed * Time.deltaTime);
3.3、主角脚本代码
综上,我们封装一个主角脚本Player.cs
,代码如下:
// Player.cs
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.AI;
/// <summary>
/// 主角脚本
/// </summary>
public class Player : MonoBehaviour
{
// 移动速度
public float speed = 1f;
// 转向速度
public float turnSpeed = 20f;
public Animator anim;
// 跟节点
public Transform rootTrans;
// 模型节点
public Transform modelTrans;
// 导航Agent
public NavMeshAgent navAgent;
// 是否在移动
private bool moving = false;
// 移动向量
private Vector3 moveDirection = Vector3.zero;
// 是否可移动
private bool canMove = true;
private void Awake() {
companyPosParticle.Stop();
homePosParticle.Stop();
}
void Update()
{
if (canMove && moving)
{
anim.SetInteger("AnimationPar", 1);
rootTrans.position += moveDirection * speed * Time.deltaTime;
modelTrans.forward = Vector3.Lerp(modelTrans.forward, moveDirection, turnSpeed * Time.deltaTime);
}
else
{
anim.SetInteger("AnimationPar", 0);
}
}
public void Move(Vector3 direction)
{
moveDirection = direction;
moving = true;
}
public void Stand()
{
moving = false;
}
将Player.cs
脚本挂到主角物体上,在Inspector
面板赋值脚本的成员变量,
我们再创建一个GameMgr.cs
脚本来调度,
// GameMgr.cs
public Player player;
public JointedArm leftJointedArm;
private Transform playerTrans;
private Transform camTrans;
// ...
// 左摇杆 -------------------------------------------
leftJointedArm.onDragCb = (direction) =>
{
var realDirect = camTrans.localToWorldMatrix * new Vector3(direction.x, 0, direction.y);
realDirect.y = 0;
realDirect = realDirect.normalized;
player.Move(realDirect);
};
leftJointedArm.onStopCb = () => { player.Stand(); };
创建一个空物体重命名为GameMgr
,把GameMgr.cs
挂到这个物体上,并在Inspector
面板中赋值脚本的成员变量。
3.4、主角移动测试
运行测试效果如下:
4、摄像机跟随
我们创建一个CameraControler.cs
脚本,实现摄像机跟随主角的逻辑,代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 摄像机控制器
/// </summary>
public class CameraControler : MonoBehaviour
{
// 限制摄像机角度范围
private const float Y_ANGLE_MIN = 10f;
private const float Y_ANGLE_MAX = 50.0f;
// 摄像机看向的物体
public Transform lookAt;
// 摄像机Transform
public Transform camTransform;
// 摄像机距离目标物体的距离
public float distance = 1.2f;
// 原始距离
private float originalDistance;
// 旋转速度
public float rotateSpeed = 0.01f;
public float currentX = 0.0f;
public float currentY = 20.0f;
private void Start()
{
camTransform = transform;
originalDistance = distance;
}
private void Update()
{
if (rotating)
{
currentX += rotateDelta.x;
currentY += rotateDelta.y;
currentY = Mathf.Clamp(currentY, Y_ANGLE_MIN, Y_ANGLE_MAX);
}
}
private void LateUpdate()
{
Vector3 dir = new Vector3(0, 0, -distance);
Quaternion rotation = Quaternion.Euler(currentY, currentX, 0);
camTransform.position = lookAt.position + rotation * dir;
camTransform.LookAt(lookAt.position);
}
}
把CameraControler.cs
脚本挂到主摄像机上,在Inspector
面板赋值脚本的成员变量,运行Unity
,可以看到有跟随效果了,
5、摄像头角度控制
我们在上面的CameraControler.cs
脚本中添加两个方法,如下:
// CameraControler.cs
private bool rotating;
private Vector2 rotateDelta;
public void RotateCam(Vector2 delta)
{
rotateDelta = delta * rotateSpeed;
rotating = true;
}
public void StopRotate()
{
rotating = false;
}
然后在GameMgr.cs
中添加右摇杆的调度,
// GameMgr.cs
// 右摇杆 ------------------------------------------
rightJointedArm.onDragCb = (direction) =>
{
camCtrler.RotateCam(direction);
};
rightJointedArm.onStopCb = () => { camCtrler.StopRotate(); };
运行Unity
,可以控制摄像头角度旋转了,
九、导航烘焙
如果我们想要实现点击地图某个位置,让主角走到目标点,可以使用Unity
的寻路导航功能,另外,这个功能也可以限制主角的移动区域,为了防止主角走到地图外面,我们使用Navigation
对场景进行导航烘焙。
首先选中地面,把地面设置为Static
,
然后点击菜单Window / AI / Navigation
,
点击Bake
分页,点击Bake
按钮,
看到地面蒙上了一层蓝色的网,就说明烘焙成功了,
你可以在场景文件所在目录中看到它生成了一个NavMesh
文件,
另外,我们需要给主角添加NavMeshAgent
组件,并根据主角模型大小设置Radius
和Height
,
如下:
运行Unity
,测试一下移动到地面边界的效果,
十、传送门触发器
传送门传送,我使用了触发器,检测主角是否通过了传送门,然后出发传送逻辑。
给传送门的前后添加两个碰撞体,并勾选Is Trigger
,
如下:
分别在两个冲送门位置添加一个标记传送目标位置的空物体,
给主角脚本Player.cs
添加传送的逻辑,
// Player.cs
public Transform homePos;
public Transform companyPos;
private string lastTrigger;
private void OnTriggerEnter(Collider other)
{
if("trigger1" == other.name || "trigger3" == other.name)
{
lastTrigger = other.name;
}
else
{
if("trigger1" == lastTrigger && "trigger2" == other.name)
{
// 执行传送,从公司到家
rootTrans.position = homePos.position;
}
else if("trigger3" == lastTrigger && "trigger4" == other.name)
{
// 执行传送,从家到公司
rootTrans.position = companyPos.position;
}
}
}
这样,当主角传过传送门的时候就会按顺序触发触发器,最终指向传送逻辑。
十一、特效
传送时加多一个特效吧,使用PhotoShop
画一个菱形,如下:
再画个菱形边框,
使用粒子系统制作特效,效果如下:
主要利用的是粒子系统的color over Lifttime
和Size over Lifetime
来达到上面的效果,
关于粒子系统的相关教程,可以参见我之前写的这几篇文章:
【游戏开发实战】权游红袍女在火中看到了什么,我看到了…(Unity | 粒子系统 | 火焰特效 | ParticleSystem | 手把手制作)
【游戏开发实战】Unity使用ShaderGraph配合粒子系统,制作子弹拖尾特效(Fate/stay night金闪闪的大招效果)
【学Unity的猫】——第十五章:Unity粒子系统ParticleSystem,下雪啦下雪啦
【游戏开发实战】手把手教你使用Unity制作一个飞机喷射火焰尾气的粒子效果
十二、最终效果
家门口:
从家门口穿过传送门:
从公司经过传送门回家:
十三、工程源码
本工程我已上传到CODE CHINA
,感兴趣的同学可自行下载学习。
地址:https://codechina.csdn.net/linxinfa/UnityPortalDemo
注:我使用的Unity
版本为Unity 2021.1.9f1c1 (64-bit)
。
十四、完毕
好了,就到这里吧,最后希望广州地铁21
号线高峰期可以提高发车频率,不然真的很痛苦。
我是林新发:https://blog.csdn.net/linxinfa
原创不易,若转载请注明出处,感谢大家~
喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信,我们下期见~