游戏理念
欲制作的游戏为一款3D解密游戏,通过剥夺玩家的视觉,来增强听觉的重要性,让玩家在黑暗中依据声音进行探索,并且随着探索点亮部分地区,最终让玩家解开谜题。
游戏底层构建
游戏的实现本身并不困难,没有涉及到太多技术层面的问题,重点在于关卡的编排和设计,因此我使用了unity自带的默认3D物体来代替各种物品,并写了简单的控制操作脚本,代码如下:
public class MoveController : MonoBehaviour
{
public Camera sight;
public Light light;
public float rotateSpeed_x;
public float rotateSpeed_y;
public float jumpSpan;
public float moveSpeed;
private Rigidbody selfBody;
private bool jumping = false;
// Start is called before the first frame update
void Start()
{
selfBody = gameObject.GetComponent<Rigidbody>();
}
// Update is called once per frame
void Update()
{
selfBody.angularVelocity = new Vector3(0, 0, 0);
var mouse_x = Input.GetAxis("Mouse X");
var mouse_y = -Input.GetAxis("Mouse Y");
transform.Rotate(0, mouse_x * rotateSpeed_x * Time.deltaTime, 0);
if (Input.GetKey(KeyCode.W))
{
if (!Input.GetKey(KeyCode.S))
transform.Translate(0, 0, moveSpeed * Time.deltaTime);
}
else if (Input.GetKey(KeyCode.S))
transform.Translate(0, 0, -moveSpeed * Time.deltaTime);
if (Input.GetKey(KeyCode.A))
{
if (!Input.GetKey(KeyCode.D))
transform.Translate(-moveSpeed * Time.deltaTime, 0, 0);
}
else if (Input.GetKey(KeyCode.D))
transform.Translate(moveSpeed * Time.deltaTime, 0, 0);
if (Input.GetKeyDown(KeyCode.Space)&&!jumping)
{
gameObject.GetComponent<Rigidbody>().AddForce(new Vector3(0, 6.5f, 0), ForceMode.Impulse);
jumping = true;
Invoke("JumpAgain", jumpSpan);
}
sight.transform.rotation = Quaternion.Euler(sight.transform.rotation.eulerAngles.x,transform.rotation.eulerAngles.y,transform.rotation.eulerAngles.z);
sight.transform.position = transform.position;
light.transform.position = transform.position;
sight.transform.Rotate(mouse_y * rotateSpeed_y * Time.deltaTime, 0, 0);
}
private void OnCollisionEnter(Collision collision)
{
if (collision.transform.CompareTag("Untouched"))
{
collision.transform.tag = "Touched";
Instantiate<Light>(light, collision.transform.position, collision.transform.rotation, collision.transform);
}
}
private void JumpAgain()
{
jumping = false;
}
}
灯光渐变代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LightShow : MonoBehaviour
{
public float targetIntensity;
public float intensityChangeSpeed;
private Light light;
void Start()
{
light = GetComponent<Light>();
}
void Update()
{
if (light.intensity < targetIntensity)
{
light.intensity += intensityChangeSpeed * Time.deltaTime;
}
else
this.enabled = false;
}
}
挂载给对应的物体生成预制体后,最简单的游戏框架就写好了,关于和场景物体的互动方式、游戏初始界面UI、游戏存档以及音频变化等代码我会在后期完善,接下来就是关卡的粗略设计。
初始房间设计
玩家在刚出生的时候对于游戏还一无所知,手中的资料仅仅只有游戏说明的大致介绍与粗略的操作方式,因此为了让玩家快速上手游戏,在游戏初期要给予引导,因此第一个房间一定要简单明了,空旷的房间内只有一个待操作物体与一扇门,将要操作的物体放在玩家的正前方,让玩家对于如何操作一目了然。
然而在玩家刚刚进入游戏时,除了眼前黑漆漆的一切之外什么都看不到,最好给予对话引导,因此我们来编写一下旁白的脚本。
旁白脚本
在场景中新建Canvas,命名为Dialog,将其绑定在MainCamera上,在其下方新建一个Text区域命名为DialogText,调整字体及位置后为其撰写出现以及消失的脚本。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class DialogShow : MonoBehaviour
{
public float fadeTime;
public float showTime;
public float continueTime;
private Text text;
private float fadeSpeed;
private float showSpeed;
private float instantiateTime;
private bool showing = false;
private bool spanning = false;
private bool disappearing = false;
private Queue<string> dialogs = new Queue<string>();
void Start()
{
text = GetComponent<Text>();
fadeSpeed = 1 / fadeTime;
showSpeed = 1 / showTime;
instantiateTime = Time.time;
}
void Update()
{
if (spanning && dialogs.Count > 0)
{
spanning = false;
text.text = dialogs.Dequeue();
showing = true;
instantiateTime = Time.time;
}
if (showing)
{
text.color = new Color(0, 0, 0, showSpeed * Time.deltaTime) + text.color;
if (text.color.a >= 1f)
showing = false;
}
if (Time.time - instantiateTime >= continueTime)
disappearing = true;
if (disappearing)
{
text.color = new Color(0, 0, 0, -fadeSpeed * Time.deltaTime) + text.color;
if (text.color.a <= 0)
{
disappearing = false;
spanning = true;
}
}
}
void ChangeContinueTime(float time)
{
continueTime = time;
}
void ShowDialog(string s)
{
dialogs.Enqueue(s);
}
}
在其他的脚本中,只要向DialogText物体使用SendMessage函数调用ShowDialog就可以显示对话,要改变持续时间则可以调用ChangeContinueTime,spanning则是用来检测当前是否有其他对话正在进行,来让触发对话的物体进行检测,并用队列储存对话,来检测当前是否有未显示的对话。
之后则是编写触发文本的脚本了,我们可以提前设想会有几种触发方式:
1、与物体交互后触发
2、走到对应区域触发
3、一段时间后触发
大致就是以上三种触发方式,因此我们要编写对应的脚本,新建三个脚本文件,分别是AreaDialog、ContactDialog与TimeDialog,对应脚本如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AreaDialog : MonoBehaviour
{
public GameObject dialogPrefeb;
public string dialog;
private void OnTriggerEnter(Collider other)
{
if (other.transform.CompareTag("Character"))
{
dialogPrefeb.SendMessage("ShowDialog", dialog);
Destroy(gameObject);
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ContactDialog : MonoBehaviour
{
public GameObject dialogPrefeb;
public string dialog;
private void OnCollisionEnter(Collision collision)
{
if (collision.transform.CompareTag("Character"))
{
dialogPrefeb.SendMessage("ShowDialog", dialog);
enabled = false;
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TimeDialog : MonoBehaviour
{
public float delayShowTime;
public GameObject dialogPrefeb;
public string dialog;
private float insTime;
private void Start()
{
insTime = Time.time;
}
private void Update()
{
if (Time.time - insTime >= delayShowTime)
{
dialogPrefeb.SendMessage("ShowDialog", dialog);
enabled = false;
}
}
}
其中互动触发和区域触发代码十分相像,是因为目前我们还没有写互动代码,互动方式暂且为触碰,因此ContactDialog日后需要修改。
区域触发需要在unity中将collider组件勾选isTrigger选框,并且监测区域只需要使用一次,因此可以直接使用Destroy来节省空间。
时间触发类多半会在某个对话触发后才触发,因此其需要有触发条件,但是我们的项目暂时还用不到时间类触发对话,因此暂时忽略它。
养成良好的编程习惯,将有关对话显示的脚本归类在一个Dialog的文件夹下。
ok,到这里我们的基础准备工作也做的差不多了,在第一个场景中加入些许对话触发,初始房间就制作完成了,让我们看看效果。
游戏的初步制作就到这里,下一次我将会制作关卡具体内容并引导玩家,谢谢大家的阅读。