如何在Unity中开发一个类FEZ的游戏(三)
By Greg Matsura(译者 @Kohlrabic )
原文地址:
前一篇:
★前言
在教程的最后一个章节中,我们将会完成我们的类FEZ游戏的搭建。现在我们已经完成了基本的关卡和角色部分。今天我们将会完成控制3D旋转功能的实现。
FEZ的旋转切换效果是通过从不同的方向来观察我们的关卡来实现的。旋转将会以90°来依次来观察我们的关卡,即前、后、左、右。
在3D场景下,我们有宽度、高度和深度需要考虑。当游戏进行时,我们的3D场景将会被分解为4个2D场景供玩家游玩。本质上场景的深度被忽略了,在屏幕上显示的平面仅保留了X和Y轴坐标。因为忽略了另一个作为深度的坐标,当玩家起跳或旋转时,将会有可能错过平台而落下。这取决于构成平台的砖块的深度坐标的不同,看起来在同一个平面上的砖块实际上来自不同的深度。所以尽管看起来玩家可以走跳到一个平台上,在实际3D场景中这个位置是没有可以立足的方块的,最终还是会错过这个平台而坠落。为了防止这种情况的发生,我们将会创建隐形的立方体invisicubes作为玩家的落脚点。这将会使玩家看起来好像真的在对应的平台上面行动。
隐形平台示意图
★旋转功能基本原理
我们需要完成的脚本基本功能如下:
当玩家没有起跳的时候,检查我们是否在一个隐形的平台之上,如果是的话,将玩家移动到距离最近的实体平台之上。这里我们将判断距离的基准设为距离摄像机的距离。当我们改变了玩家的深度属性的时候,需要更新隐形立方体的位置与之对应。
提示:如果我们允许玩家站在隐形平台上进行旋转操作,这将会不利于我们的管理,并且会出现更多的麻烦。这就是为什么我们需要将玩家移动到正确的实体平台的原因。
★实现旋转功能
在FEZ中,核心玩法是通过旋转来探索世界,解决不同的谜题。通过正交摄像机我们每次只能看到3D世界的一个平面。这意味着去除了景深,所有的物体在屏幕上的显示仿佛都在一个平面之上。我们只会在旋转操作时感受到这是一个3D的场景,剩下的时间中感觉就像是在游玩一个2D游戏一样。
我们从制作一个隐形的立方体开始。我们将会大量地复用它。首先,在你的场景里创建一个新的立方体,将它取名为Invisicube。现在我们要做的就是去除(或者关闭)立方体的MeshRenderer组件,将这个立方体拖拽到Assets窗口创建为预制体。
提示:位置并不重要,但是Box Collider是必须的。去除(或者关闭)立方体的Mesh Renderer组件。
现在我们将会创建一个新的脚本来实现功能。这个脚本有一些长,但是我会对要点做出解释。创建一个新的脚本命名为FEZManager。
代码如下:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
///
///这个脚本旨在创建可以供玩家行动的隐形砖块。游戏的机制是探索允许旋转的3D关卡,这意味着角色在不同时刻有可能处于不同的深度坐标,有可能会无法保持始终立足在我们创建的实体平台之上。
///在实际游玩中我们看到的角色是在2D平面中行动的,角色与不同深度的砖块看起来是在同一个平面上的。但是一旦它们的深度值不同,我们就需要创建供角色落脚的隐形平台。
///当时机成熟时,我们就会将玩家转移到距离最近的实体平台,这样我们就不会暴露我们游戏的机制了。
///
public class FezManager : MonoBehaviour {
//玩家移动及朝向
private FezMove fezMove;
public FacingDirection facingDirection;
public GameObject Player;
//设置旋转角度
private float degree = 0;
//实体砖块
public Transform Level;
//建筑砖块
public Transform Building;
//隐形砖块
public GameObject InvisiCube;
private List<Transform> InvisiList = new List<Transform>();
//上一帧玩家的朝向,深度,用来避免无意义的重复构建隐形砖块位置
private FacingDirection lastfacing;
private float lastDepth = 0f;
//单元尺寸,所有立方体砖块的尺寸应与此保持一致
public float WorldUnits = 1.000f;
void Start () {
facingDirection = FacingDirection.Front;
fezMove = Player.GetComponent<FezMove> ();
UpdateLevelData (true);
}
void Update () {
//控制玩家当前深度逻辑
//如果玩家当前处于一个隐形平台之上,尝试转移到实体平台之上,这可以避免旋转时暴露我们的逻辑
//尽可能转移到距离摄像机最近的平台,这可以使我们的旋转时的效果更加自然
if(!fezMove._jumping)
{
bool updateData = false;
if(OnInvisiblePlatform())
if(MovePlayerDepthToClosestPlatform())
updateData = true;
if(MoveToClosestPlatformToCamera())
updateData = true;
if(updateData)
UpdateLevelData(false);
}
//旋转操作
if(Input.GetKeyDown(KeyCode.RightArrow))
{
//当进行旋转操作时,必须保持玩家处于实体平台之上,否则旋转之后我们有可能会处于半空之中。
if(OnInvisiblePlatform())
{
//MoveToClosestPlatform();
MovePlayerDepthToClosestPlatform();
}
lastfacing = facingDirection;
facingDirection = RotateDirectionRight();
degree-=90f;
UpdateLevelData(false);
fezMove.UpdateToFacingDirection(facingDirection, degree);
}
else if( Input.GetKeyDown(KeyCode.LeftArrow))
{
if(OnInvisiblePlatform())
{
//MoveToClosestPlatform();
MovePlayerDepthToClosestPlatform();
}
lastfacing = facingDirection;
facingDirection = RotateDirectionLeft();
degree+=90f;
UpdateLevelData(false);
fezMove.UpdateToFacingDirection(facingDirection, degree);
}
}
/// 摧毁之前的隐形平台
/// 根据玩家的朝向和当前的2D平面来创建新的隐形平台
private void UpdateLevelData(bool forceRebuild)
{
//If facing direction and depth havent changed we do not need to rebuild
if(!forceRebuild)
if (lastfacing == facingDirection && lastDepth == GetPlayerDepth ())
return;
foreach(Transform tr in InvisiList)
{
//Move obsolete invisicubes out of the way and delete
tr.position = Vector3.zero;
Destroy(tr.gameObject);
}
InvisiList.Clear ();
float newDepth = 0f;
newDepth = GetPlayerDepth ();
CreateInvisicubesAtNewDepth (newDepth);
}
///
/// 判断玩家是否站在一个隐形平台之上
///
private bool OnInvisiblePlatform()
{
foreach(Transform item in InvisiList)
{
if(Mathf.Abs(item.position.x - fezMove.transform.position.x) < WorldUnits && Mathf.Abs(item.position.z - fezMove.transform.position.z) < WorldUnits)
if(fezMove.transform.position.y - item.position.y <= WorldUnits + 0.2f && fezMove.transform.position.y - item.position.y >0)
return true;
}
return false;
}
///
/// 将玩家传送到与摄像机高度一致,且距离最近的砖块
/// 仅支持单元尺度为1的砖块
///
private bool MoveToClosestPlatformToCamera()
{
bool moveCloser = false;
foreach(Transform item in Level)
{
if(facingDirection == FacingDirection.Front || facingDirection == FacingDirection.Back)
{
if(Mathf.Abs(item.position.x - fezMove.transform.position.x) < WorldUnits +0.1f)
{
if(fezMove.transform.position.y - item.position.y <= WorldUnits + 0.2f && fezMove.transform.position.y - item.position.y >0 && !fezMove._jumping)
{
if(facingDirection == FacingDirection.Front && item.position.z < fezMove.transform.position.z)
moveCloser = true;
if(facingDirection == FacingDirection.Back && item.position.z > fezMove.transform.position.z)
moveCloser = true;
if(moveCloser)
{
fezMove.transform.position = new Vector3(fezMove.transform.position.x, fezMove.transform.position.y, item.position.z);
return true;
}
}
}
}
else{
if(Mathf.Abs(item.position.z - fezMove.transform.position.z) < WorldUnits + 0.1f)
{
if(fezMove.transform.position.y - item.position.y <= WorldUnits + 0.2f && fezMove.transform.position.y - item.position.y >0 && !fezMove._jumping)
{
if(facingDirection == FacingDirection.Right && item.position.x > fezMove.transform.position.x)
moveCloser = true;
if(facingDirection == FacingDirection.Left && item.position.x < fezMove.transform.position.x)
moveCloser = true;
if(moveCloser)
{
fezMove.transform.position = new Vector3(item.position.x, fezMove.transform.position.y, fezMove.transform.position.z);
return true;
}
}
}
}
}
return false;
}
/// 查询隐形砖块列表
private bool FindTransformInvisiList(Vector3 cube)
{
foreach(Transform item in InvisiList)
{
if(item.position == cube)
return true;
}
return false;
}
/// 查询实体砖块列表
private bool FindTransformLevel(Vector3 cube)
{
foreach(Transform item in Level)
{
if(item.position == cube)
return true;
}
return false;
}
/// 判断相机和砖块之间是否有其他的建筑方块
private bool FindTransformBuilding(Vector3 cube)
{
foreach(Transform item in Building)
{
if(facingDirection == FacingDirection.Front )
{
if(item.position.x == cube.x && item.position.y == cube.y && item.position.z < cube.z)
return true;
}
else if(facingDirection == FacingDirection.Back )
{
if(item.position.x == cube.x && item.position.y == cube.y && item.position.z > cube.z)
return true;
}
else if(facingDirection == FacingDirection.Right )
{
if(item.position.z == cube.z && item.position.y == cube.y && item.position.x > cube.x)
return true;
}
else
{
if(item.position.z == cube.z && item.position.y == cube.y && item.position.x < cube.x)
return true;
}
}
return false;
}
/// 当玩家跳到一个隐形平台上时,将玩家转移到高度相同距离最近的实体平台之上
private bool MovePlayerDepthToClosestPlatform()
{
foreach(Transform item in Level)
{
if(facingDirection == FacingDirection.Front || facingDirection == FacingDirection.Back)
{
if(Mathf.Abs(item.position.x - fezMove.transform.position.x) < WorldUnits + 0.1f)
if(fezMove.transform.position.y - item.position.y <= WorldUnits + 0.2f && fezMove.transform.position.y - item.position.y >0)
{
fezMove.transform.position = new Vector3(fezMove.transform.position.x, fezMove.transform.position.y, item.position.z);
return true;
}
}
else
{
if(Mathf.Abs(item.position.z - fezMove.transform.position.z) < WorldUnits + 0.1f)
if(fezMove.transform.position.y - item.position.y <= WorldUnits + 0.2f && fezMove.transform.position.y - item.position.y >0)
{
fezMove.transform.position = new Vector3(item.position.x, fezMove.transform.position.y, fezMove.transform.position.z);
return true;
}
}
}
return false;
}
///创建隐形平台逻辑
private Transform CreateInvisicube(Vector3 position)
{
GameObject go = Instantiate (InvisiCube) as GameObject;
go.transform.position = position;
return go.transform;
}
private void CreateInvisicubesAtNewDepth(float newDepth)
{
Vector3 tempCube = Vector3.zero;
foreach(Transform child in Level)
{
if(facingDirection == FacingDirection.Front || facingDirection == FacingDirection.Back)
{
tempCube = new Vector3(child.position.x, child.position.y, newDepth);
if(!FindTransformInvisiList(tempCube) && !FindTransformLevel(tempCube) && !FindTransformBuilding(child.position))
{
Transform go = CreateInvisicube(tempCube);
InvisiList.Add(go);
}
}
//仅改变当前深度坐标,对其他坐标不做出改变
else if(facingDirection == FacingDirection.Right || facingDirection == FacingDirection.Left)
{
tempCube = new Vector3(newDepth, child.position.y, child.position.z);
if(!FindTransformInvisiList(tempCube) && !FindTransformLevel(tempCube) && !FindTransformBuilding(child.position))
{
Transform go = CreateInvisicube(tempCube);
InvisiList.Add(go);
}
}
}
}
/// 当需要使玩家返回起始处时
public void ReturnToStart()
{
UpdateLevelData (true);
}
/// 根据玩家朝向判断并返回当前深度坐标值
private float GetPlayerDepth()
{
float ClosestPoint = 0f;
if(facingDirection == FacingDirection.Front || facingDirection == FacingDirection.Back)
{
ClosestPoint = fezMove.transform.position.z;
}
else if(facingDirection == FacingDirection.Right || facingDirection == FacingDirection.Left)
{
ClosestPoint = fezMove.transform.position.x;
}
return Mathf.Round(ClosestPoint);
}
/// 当我们向右旋转后重新判断玩家朝向
private FacingDirection RotateDirectionRight()
{
int change = (int)(facingDirection);
change++;
//Our FacingDirection enum only has 4 states, if we go past the last state, loop to the first
if (change > 3)
change = 0;
return (FacingDirection) (change);
}
/// 当我们向左旋转后重新判断玩家朝向
private FacingDirection RotateDirectionLeft()
{
int change = (int)(facingDirection);
change--;
if (change < 0)
change = 3;
return (FacingDirection) (change);
}
}
//枚举,用于判断当前朝向
public enum FacingDirection
{
Front = 0,
Right = 1,
Back = 2,
Left = 3
}
当我们开始游戏,这个脚本将会通过player的位置来决定在哪一个深度用隐形的立方体来创建平台。我们在脚本的末尾添加了关于player朝向的枚举。我们通过这种方式来追踪player实时的朝向。当player的朝向在X轴时,决定深度就是Z轴;当player的朝向在Z轴时,决定深度的就是X轴。每当玩家按下键盘方向键的右或者左箭头时,我们将会向着相应的方向旋转player 90°。因为我们的摄像机是player的子物体,它也会自动地跟着旋转,我们的player将会始终面对摄像机,这非常的便利。
在下面对控制器脚本做出更新,我们将会从判断Player是否起跳的功能开始,这将成为我们判定当前是否能够安全地改变Player的深度。如果我们站在一个隐形的平台上面,我们能够知道玩家至少有两个方向的坐标是和构成实际平台的某一个方块相同的。例如,如果Player面朝的方向是前方(Z轴),那么在玩家X轴方向上至少会有一个实体方块。Y轴坐标也应该和这个方块一致,但是因为Character Controller的size可能有些许的误差。Z轴坐标是唯一的差异,相对于玩家,它有可能是正,也可能是负。
通读相关的代码将会让我们更加清楚发生了什么。接下来,我们将要给FezMove脚本做出一些改变,为它添加旋转的功能。我们将会实时检测玩家的朝向和移动方向,来决定我们世界的旋转方向。
下面就是修改后的脚本:
using UnityEngine;
using System.Collections;
public class FezMove : MonoBehaviour {
private int Horizontal = 0;
public Animator anim;
public float MovementSpeed = 5f;
public float Gravity = 1f;
public CharacterController charController;
private FacingDirection _myFacingDirection;
public float JumpHeight = 0f;
public bool _jumping = false;
private float degree = 0;
public FacingDirection CmdFacingDirection {
set{ _myFacingDirection = value;
}
}
// Update is called once per frame
void Update () {
if (Input.GetAxis (&#34;Horizontal&#34;) < 0)
Horizontal = -1;
else if (Input.GetAxis (&#34;Horizontal&#34;) > 0)
Horizontal = 1;
else
Horizontal = 0;
if (Input.GetKeyDown (KeyCode.Space) && !_jumping)
{
_jumping = true;
StartCoroutine(JumpingWait());
}
if(anim)
{
anim.SetInteger(&#34;Horizontal&#34;, Horizontal);
float moveFactor = MovementSpeed * Time.deltaTime * 10f;
MoveCharacter(moveFactor);
}
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.Euler(0, degree, 0), 8 * Time.deltaTime);
}
private void MoveCharacter(float moveFactor)
{
Vector3 trans = Vector3.zero;
if(_myFacingDirection == FacingDirection.Front)
{
trans = new Vector3(Horizontal* moveFactor, -Gravity * moveFactor, 0f);
}
else if(_myFacingDirection == FacingDirection.Right)
{
trans = new Vector3(0f, -Gravity * moveFactor, Horizontal* moveFactor);
}
else if(_myFacingDirection == FacingDirection.Back)
{
trans = new Vector3(-Horizontal* moveFactor, -Gravity * moveFactor, 0f);
}
else if(_myFacingDirection == FacingDirection.Left)
{
trans = new Vector3(0f, -Gravity * moveFactor, -Horizontal* moveFactor);
}
if(_jumping)
{
transform.Translate( Vector3.up * JumpHeight * Time.deltaTime);
}
charController.SimpleMove (trans);
}
public void UpdateToFacingDirection(FacingDirection newDirection, float angle)
{
_myFacingDirection = newDirection;
degree = angle;
}
public IEnumerator JumpingWait()
{
yield return new WaitForSeconds (0.35f);
//Debug.Log (&#34;Returned jump to false&#34;);
_jumping = false;
}
}
要使用这个脚本,首先创建一个空物体,用于挂载这个脚本。如图所示,你需要将相应的Game Object填入脚本的选项中。
★平台和建筑物(Platforms & Buildings)
关卡的具体设计完全取决于你自己,但是,这里有一些关键点你或许需要提前了解一下。或许可以帮助你更加轻松地完成关卡的搭建。
搭建关卡时,记得将立方体分别放置在对应的空物体中进行分类。
从中心的支柱为基础,围绕着它开始搭建我们的关卡,不要忘记使用我们之前教程中的自动对齐脚本(AutoSnap)来帮助提升你的效率。
这里是我搭建的一个示例关卡
旋转时的效果
以上就是此篇教程的全部内容了,感谢你的阅读!
页:
[1]