如何制作一个迷你 Minecraft 游戏





5.00/5 (4投票s)
编程本身就不是一件容易的事。图形编程会使事情变得更加复杂!
引言
距离我上次在CodeProject发表文章已经有一段时间了。最近,我决定写一篇新文章,希望有人能从中受益。
在这篇文章中,您将看到如何创建一个应用程序,让您能够构建一个类似我的世界的世界。当然,我们不会构建整个应用程序,但我会为您提供足够的内容,让您能够开始并在此基础上进行构建。
基本上,我们将创建一个虚拟环境,用户可以在其中使用方块建造东西!就是这么简单。就像孩子们在现实世界中使用积木创造有趣的结构一样,您也可以在虚拟世界中这样做。
上图应该能让您对我们正在实现的目标有一个大致的了解。
背景
构思这个项目是在我考虑为低年级计算机图形学课程设计一个项目时产生的。这是一个相对简单的想法/概念,对于学生来说是一个很好的实践编程练习。
您需要熟悉Unity游戏引擎和C#语言。我不会涵盖这两者的基础知识!如果您从未接触或听说过Unity,我有一个10部分文章系列,涵盖了基础知识,链接是第一部分。我建议您从那里开始!
本文将学习的概念和/或主题
- GameObject
- 预制件
- 纹理和材质
- 法线向量
- 基本用户界面
- 灯光和摄像机
Using the Code
在我们开始之前,您需要获取Unity。您可以从www.unity3d.com下载。
您可以下载Windows和Mac操作系统的安装程序。本文中的所有内容都适用于这两个平台。主要区别在于键盘快捷键,除此之外,其他一切应该都是一样的。
Unity包:代码
概念
现在基本的东西都讲完了,我们可以开始讨论我们想要实现的目标以及如何实现。至少,有一种实现方法。以下是一些我们需要考虑的事项:
- 使用鼠标放置对象
- 更改对象的纹理/材质
- 能够移动摄像机,以便从不同角度查看环境
看起来很简单。让我们来看看如何开始实现它们。
表示方块
我们需要某种方式来表示我们要放置在世界中并/或与之交互的对象。要做到这一点,我们需要创建我们想要表示的对象的几何模型,并赋予它一些材质,以表示我们将应用于几何体的颜色或纹理。
我们将使用单位立方体作为我们的模型。它是最简单、最容易表示和处理的3D形状之一。
一个立方体由8个顶点、6个面和12个三角形组成。这是一个很好的链接,可以帮助您掌握基本术语。
为了成功表示我们的立方体,我们需要所谓的Mesh Filter。几何体以及每个顶点和边、边和面之间的所有关系都存储在此处。从技术上讲,它还包含一些我们稍后会看到的重要数据。
我们还需要一种方法来在3D世界中定位我们的立方体。
场景中的每个对象都是一个GameObject
。无论它们代表什么,模型、灯光、摄像机等,它们都是GameObject
。每个GameObject
都有一个Transform Component,它在GO在3D世界中的定位方面非常重要。Transform
组件包含三个Vector,每个Vector都代表以下内容:
职位
旋转
Scale
接下来,我们需要一种方法来为立方体着色。这通过Mesh Renderer和与立方体关联的Material来完成。当您在Unity中创建立方体原始体时,它会配置GameObject
,使其具有能够渲染为立方体的基本组件。
还有一个您应该了解的最后组件,那就是Collider。引擎使用它来检测碰撞。稍后您将看到我们如何使用它。
创建预制件并应用默认材质
现在您对GameObject
有了基本了解。我们将需要一个预制件,以便在运行时实例化以放置我们的方块。最好的展示方式是通过一些屏幕截图。
在上图中,您将看到Unity IDE。您应该关注的主要事情是预制件的创建。要创建预制件,只需从Hierarchy Window中选择代表Cube
的GameObject
,然后将其拖动到Projects Window下的Asset文件夹中。预制件基本上是您当时GameObject
的已保存实例。我们将使用此预制件来动态生成我们想要放置在我们世界中的方块。
如果您花时间查看上图的高分辨率版本,您将直观地看到我们讨论过的每个组件是如何显示的。
注意:如果您对Unity没有任何经验,我鼓励您花时间阅读10部分系列文章。
纹理和材质
为简单起见,我们将使用三种纹理/材质,可以在实例化时应用于立方体。这些可以是任何纹理。出于演示目的,我将使用以下三种:
您可以直接下载这些纹理,或者使用自己的纹理来创建三个独特的材质。要创建材质,您需要右键单击Projects Window,然后从弹出窗口中选择Create Material。为材质资源起一个唯一的名称,并将一个纹理与Albeido
属性的Texture属性关联。
您需要为每种纹理/材质执行此步骤。
游戏/应用逻辑
现在您已经准备好了基础,是时候开始做有趣的部分了!我们需要创建一些C#代码来将所有内容整合在一起。
首先,我们需要有一个地板或基础。这将用于放置我们的初始方块,它将成为构建方块的平台。我在这里图示的地板位于<0,0,0>,比例为<10,0.1,10>。半透明的黄色方块是一个视觉指示器,它会反馈给用户,并根据鼠标位置告诉用户方块将被放置在哪里。我还应用了一个未提供的棋盘格纹理。
这是使您能够执行图中所示操作的代码:
bool realTimeHit =
Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition),
out realTimeHitInfo);
if (realTimeHit)
{
{
if (realtimeCube == null)
{
realtimeCube = GameObject.CreatePrimitive(PrimitiveType.Cube);
realtimeCube.name = "TempCube";
realtimeCube.layer = 2;
realtimeCube.AddComponent<Rigidbody>().useGravity = false;
realtimeCube.AddComponent<BlockCollision>();
realtimeCube.GetComponent<Renderer>().material =
transparentMaterial;
realtimeCube.GetComponent<Renderer>().material.color =
new Color(1, 1, 0, 0.5f);
realtimeCube.transform.position =
new Vector3(realTimeHitInfo.point.x,
realTimeHitInfo.point.y + (0.5f),
realTimeHitInfo.point.z);
}
else
{
// check to see if we
if(realTimeHitInfo.transform.tag.Equals("Base"))
{
realtimeCube.GetComponent<Renderer>().material.color =
new Color(1, 1, 0, 0.5f);
realtimeCube.transform.position =
Vector3.Lerp(realtimeCube.transform.position,
new Vector3(realTimeHitInfo.point.x,
realTimeHitInfo.point.y + (0.5f),
realTimeHitInfo.point.z),
Time.deltaTime * 10);
}
else
{
realtimeCube.GetComponent<Renderer>().material.color =
new Color(0, 1, 0, 0.5f);
if (realTimeHitInfo.normal == new Vector3(0, 0, 1))
{
realtimeCube.transform.position =
new Vector3(realTimeHitInfo.transform.position.x,
realTimeHitInfo.transform.position.y,
realTimeHitInfo.point.z + (0.5f));
}
if (realTimeHitInfo.normal == new Vector3(1, 0, 0))
{
realtimeCube.transform.position =
new Vector3(realTimeHitInfo.point.x + (0.5f),
realTimeHitInfo.transform.position.y,
realTimeHitInfo.transform.position.z);
}
if (realTimeHitInfo.normal == new Vector3(0, 1, 0))
{
realtimeCube.transform.position =
new Vector3(realTimeHitInfo.transform.position.x,
realTimeHitInfo.point.y + (0.5f),
realTimeHitInfo.transform.position.z);
}
if (realTimeHitInfo.normal == new Vector3(0, 0, -1))
{
realtimeCube.transform.position =
new Vector3(realTimeHitInfo.transform.position.x,
realTimeHitInfo.transform.position.y,
realTimeHitInfo.point.z - (0.5f));
}
if (realTimeHitInfo.normal == new Vector3(-1, 0, 0))
{
realtimeCube.transform.position =
new Vector3(realTimeHitInfo.point.x - (0.5f),
realTimeHitInfo.transform.position.y,
realTimeHitInfo.transform.position.z);
}
if (realTimeHitInfo.normal == new Vector3(0, -1, 0))
{
realtimeCube.transform.position =
new Vector3(realTimeHitInfo.transform.position.x,
realTimeHitInfo.point.y - (0.5f),
realTimeHitInfo.transform.position.z);
}
}
}
}
}
else
{
if (realtimeCube)
Destroy(realtimeCube);
}
}
else
{
if (realtimeCube)
Destroy(realtimeCube);
}
上面的代码可能看起来令人困惑,但请相信我,大部分复杂性都已在幕后处理!让我们再次分解并理解概念,然后查看代码以将两者联系起来。
我们首先需要做的是获取屏幕空间中的鼠标位置,并将该2D坐标转换为3D世界空间。接下来,我们需要创建一个射线,该射线将从新计算出的3D坐标生成,并以给定距离投射到世界中。这就是所谓的Raycasting。
Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out realTimeHitInfo);
此函数使用物理引擎从空间中的一个点到另一个点生成一条射线,并将它收集的数据存储在类型为RaycastHit
的对象中,作为out
参数,即realTimeHitInfo
。实际函数返回一个布尔变量,指示射线是否与世界中的任何物体发生碰撞,此布尔值存储在realTimeHit
变量中。
如果为true,我们则提取在realTimeHitInfo
变量中存储的我们检测/碰撞到的对象。接下来,我们确定是否已实例化我们的临时立方体,如果没有,我们就实例化立方体原始体并分配基本属性。
if (realtimeCube == null)
{
realtimeCube = Instantiate(buildingBlockPref);
realtimeCube.name = "TempCube";
realtimeCube.layer = LayerMask.NameToLayer("RealTime");
realtimeCube.AddComponent<BlockCollision>();
realtimeCube.GetComponent<Renderer>().material = transparentMaterial;
realtimeCube.GetComponent<Renderer>().material.color = new Color(1, 1f, 0, 0.5f);
realtimeCube.transform.position =
new Vector3(realTimeHitInfo.point.x,
realTimeHitInfo.point.y + (0.5f),
realTimeHitInfo.point.z);
}
else
子句将使用鼠标指针并将半透明立方体移动到新位置。如果您恰好指向基础,它将将其放置在与基础碰撞的指定位置。如果您恰好与现有方块碰撞,您将需要确定您碰撞的面/侧。这是通过计算或获取法线向量来完成的,该向量将确定您碰撞的面。使用此信息,您将调整方块的放置,使其相应地吸附到面上。
else
{
realtimeCube.GetComponent<Renderer>().material.color = new Color(0, 1, 0, 0.5f);
if (realTimeHitInfo.normal == new Vector3(0, 0, 1))
{
realtimeCube.transform.position =
new Vector3(realTimeHitInfo.transform.position.x,
realTimeHitInfo.transform.position.y,
realTimeHitInfo.point.z + (0.5f));
}
if (realTimeHitInfo.normal == new Vector3(1, 0, 0))
{
realtimeCube.transform.position =
new Vector3(realTimeHitInfo.point.x + (0.5f),
realTimeHitInfo.transform.position.y,
realTimeHitInfo.transform.position.z);
}
if (realTimeHitInfo.normal == new Vector3(0, 1, 0))
{
realtimeCube.transform.position =
new Vector3(realTimeHitInfo.transform.position.x,
realTimeHitInfo.point.y + (0.5f),
realTimeHitInfo.transform.position.z);
}
if (realTimeHitInfo.normal == new Vector3(0, 0, -1))
{
realtimeCube.transform.position =
new Vector3(realTimeHitInfo.transform.position.x,
realTimeHitInfo.transform.position.y,
realTimeHitInfo.point.z - (0.5f));
}
if (realTimeHitInfo.normal == new Vector3(-1, 0, 0))
{
realtimeCube.transform.position =
new Vector3(realTimeHitInfo.point.x - (0.5f),
realTimeHitInfo.transform.position.y,
realTimeHitInfo.transform.position.z);
}
if (realTimeHitInfo.normal == new Vector3(0, -1, 0))
{
realtimeCube.transform.position =
new Vector3(realTimeHitInfo.transform.position.x,
realTimeHitInfo.point.y - (0.5f),
realTimeHitInfo.transform.position.z);
}
}
以上代码最重要的部分是法线向量的方向。我们实际上需要检查六个法线向量,每个面一个。一旦确定了这一点,我们将重新定位realtimeCube
的位置,使其吸附到我们碰撞的面。
差不多就是这样了。我们将使用鼠标左键永久地将方块放置在世界中。
这可以通过以下代码片段完成:
if (Input.GetMouseButtonUp(0))
{
//Destroy(realtimeCube);
#region Screen To World
RaycastHit hitInfo = new RaycastHit();
bool hit
= Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition),
out hitInfo, 50, applyMask);
if (hit)
{
if (Explode)
{
var t = hitInfo.transform.GetComponent<TriangleExplosion>();
StartCoroutine(t.SplitMesh(true));
return;
}
var cube = Instantiate(buildingBlockPref);
cube.tag = "MyCube";
cube.name = $"Cube{index}"; index++;
cube.AddComponent<TriangleExplosion>();
cube.GetComponent<BoxCollider>().isTrigger = true;
cube.GetComponent<Renderer>().material = blockMaterial;
BlockHistory.Add(cube);
cube.transform.position =
new Vector3(hitInfo.point.x,
hitInfo.point.y+0.5f,
hitInfo.point.z);
#region HIDE
if (hitInfo.transform.tag.Equals("Base"))
{
cube.transform.position =
new Vector3(hitInfo.point.x,
hitInfo.point.y + (0.5f),
hitInfo.point.z);
}
#region HIDE
else
{
if (hitInfo.normal == new Vector3(0, 0, 1))
{
cube.transform.position =
new Vector3(hitInfo.transform.position.x,
hitInfo.transform.position.y,
hitInfo.point.z + (0.5f));
}
if (hitInfo.normal == new Vector3(1, 0, 0))
{
cube.transform.position =
new Vector3(hitInfo.point.x + (0.5f),
hitInfo.transform.position.y,
hitInfo.transform.position.z);
}
if (hitInfo.normal == new Vector3(0, 1, 0))
{
cube.transform.position =
new Vector3(hitInfo.transform.position.x,
hitInfo.point.y + (0.5f),
hitInfo.transform.position.z);
}
if (hitInfo.normal == new Vector3(0, 0, -1))
{
cube.transform.position =
new Vector3(hitInfo.transform.position.x,
hitInfo.transform.position.y,
hitInfo.point.z - (0.5f));
}
if (hitInfo.normal == new Vector3(-1, 0, 0))
{
cube.transform.position =
new Vector3(hitInfo.point.x - (0.5f),
hitInfo.transform.position.y,
hitInfo.transform.position.z);
}
if (hitInfo.normal == new Vector3(0, -1, 0))
{
cube.transform.position =
new Vector3(hitInfo.transform.position.x,
hitInfo.point.y - (0.5f),
hitInfo.transform.position.z);
}
}
#endregion
//Debug.DrawRay(hitInfo.point, hitInfo.normal, Color.red, 2, false);
//Debug.Log(hitInfo.normal);
#endregion
}
else
{
Debug.Log("No hit");
}
#endregion
}
这就是我们在小世界中放置方块所需的一切!
基本用户界面
最后,我们需要一种方法来更改或选择我们的材质。这将通过屏幕底部的三个简单的按钮选择来完成。
每个按钮都会在OnClick
事件上调用以下函数。您可以传递基本参数来确定要更改为哪种材质。
public void ChangeMaterial(Button button)
{
selectedMaterialIndex = Convert.ToInt32(button.name.Last().ToString()) - 1;
blockMaterial = availableMaterials[selectedMaterialIndex];
if (OnMaterialChanged != null)
OnMaterialChanged(blockMaterial);
}
这就是它的全部内容。
关注点
这是一个简单的项目,可以扩展成一个很酷的小工具,用于您自己的游戏创意。您应该尝试扩展的一些项目是:
- 放置方块后对其进行编辑
- 扩展基础以获得更多空间
- 保存您的方块设计供下次使用
以上三个要点都应该很容易实现,并且对于应用程序来说功能非常强大。
历史
- 2019年12月5日:首次发布。方块放置基础。