2019 年 9 月—2019 年 12 月|学校项目
Trials of the Archmage 是一款使用魔法解谜的奇幻游戏。
女巫 Maeve 只有一个目标:成为这个时代最伟大的巫师——Archmage。为了实现它,她没有留在魔法学院被动学习理论,而是踏入 Forlorn Forest,挑战一系列神秘试炼“Trials of the Archmage”,证明自己对火焰与寒冰魔法的掌控。
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using PDollarGestureRecognizer;
public class GestureController : MonoBehaviour {
public Transform gestureOnScreenPrefab;
private List trainingSet = new List();
private List points = new List();
private int strokeId = -1;
private Vector3 virtualKeyPosition = Vector2.zero;
private RuntimePlatform platform;
private int vertexCount = 0;
public GameObject gestureEffect;
private List gestureLinesRenderer = new List();
private LineRenderer currentGestureLineRenderer;
private SpellCastingController spellCastingController;
private bool isDrawingGesture = false;
private bool isGestureButtonDown = false;
private bool isWaitingForGesture = false;
//GUI
[HideInInspector]
public string message;
[HideInInspector]
public float score;
private bool recognized;
private string newGestureName = "";
private GameObject currentGestureEffect = null;
// A buffer for continuous drawings
public float gestureBuffer = 0.1f;
void Start () {
spellCastingController = GameObject.Find("GameManagement/Controllers/SpellCastingController").GetComponent();
platform = Application.platform;
//Load pre-made gestures
TextAsset[] gesturesXml = Resources.LoadAll("GestureSet/10-stylus-MEDIUM/");
foreach (TextAsset gestureXml in gesturesXml)
trainingSet.Add(GestureIO.ReadGestureFromXML(gestureXml.text));
//Load user custom gestures
//Debug.Log(Application.persistentDataPath);
string[] filePaths = Directory.GetFiles(Application.persistentDataPath, "*.xml");
foreach (string filePath in filePaths)
trainingSet.Add(GestureIO.ReadGestureFromFile(filePath));
}
void Update () {
if (platform == RuntimePlatform.Android || platform == RuntimePlatform.IPhonePlayer) {
if (Input.touchCount > 0) {
virtualKeyPosition = new Vector3(Input.GetTouch(0).position.x, Input.GetTouch(0).position.y);
}
} else {
if (Input.GetMouseButton(0)) {
virtualKeyPosition = new Vector3(Input.mousePosition.x, Input.mousePosition.y);
}
}
if (currentGestureEffect != null) {
//Debug.Log(currentGestureEffect.transform.position);
currentGestureEffect.transform.position = Camera.main.ScreenToWorldPoint(new Vector3(virtualKeyPosition.x, virtualKeyPosition.y, 10));
}
if (isDrawingGesture && !isWaitingForGesture) {
points.Add(new Point(virtualKeyPosition.x, -virtualKeyPosition.y, strokeId));
if (currentGestureLineRenderer != null) {
currentGestureLineRenderer.SetVertexCount(++vertexCount);
currentGestureLineRenderer.SetPosition(vertexCount - 1, Camera.main.ScreenToWorldPoint(new Vector3(virtualKeyPosition.x, virtualKeyPosition.y, 10)));
}
}
}
public void StartGestureDrawing(){
isGestureButtonDown = true;
// New Gesture!
if (!isDrawingGesture) {
Transform tmpGesture = Instantiate(gestureOnScreenPrefab, transform.position, transform.rotation) as Transform;
currentGestureLineRenderer = tmpGesture.GetComponent();
currentGestureLineRenderer.SetVertexCount(0);
vertexCount = 0;
currentGestureEffect = Instantiate(gestureEffect, transform.position, Quaternion.identity);
}
isDrawingGesture = true;
}
public void EndGestureDrawing(){
isGestureButtonDown = false;
StartCoroutine(LateEndDrawing());
}
IEnumerator LateEndDrawing() {
isWaitingForGesture = true;
yield return new WaitForSecondsRealtime(gestureBuffer);
isWaitingForGesture = false;
if (!isGestureButtonDown) {
if (isDrawingGesture) {
// It means we get a gesture now!
Recognize();
spellCastingController.EndDrawingGesture(message, score);
ClearStrokes();
}
isDrawingGesture = false;
}
}
public void ClearStrokes(){
strokeId = -1;
points.Clear();
StartCoroutine(LateDestroy(currentGestureEffect.gameObject, 2f));
currentGestureEffect = null;
currentGestureLineRenderer.SetVertexCount(0);
Destroy(currentGestureLineRenderer.gameObject);
}
IEnumerator LateDestroy(GameObject go, float waitTime)
{
yield return new WaitForSeconds(waitTime);
Destroy(go);
}
List CleanUpNearPoints(List points) {
List newPoints = new List();
newPoints.Add(points[0]);
for (int i = 1; i < points.Count - 1; i++) {
if (Point.GetDistance(points[i], points[i - 1]) > 5)
newPoints.Add(points[i]);
}
return newPoints;
}
void Recognize(){
recognized = true;
List pointsForRecognize = CleanUpNearPoints(points);
Gesture candidate = new Gesture(CleanUpNearPoints(points).ToArray());
// A random touch on screen
if (pointsForRecognize.Count == 1) {
message = "NULL";
score = 0;
return;
}
Result gestureResult = PointCloudRecognizer.Classify(candidate, trainingSet.ToArray());
message = gestureResult.GestureClass;
score = gestureResult.Score;
//Debug.Log(gestureResult.GestureClass + " " + gestureResult.Score);
}
}
该 GestureController 脚本负责处理玩家的手势输入,并与数据库比对以识别玩家所画的手势。
施法系统 是本作的亮点。玩家通过真实手势亲手施放魔法,而不是点击“技能按钮”。
施法系统由两部分组成。第一部分是选择对象:玩家需要点击想要交互的物体;第二部分是绘制手势:玩家画出特定手势来召唤魔法。
在实现手势系统时,我参考了 $P Point-Cloud Recognizer 进行点集识别,实际效果很好。最终设计中,“V”手势对应火焰魔法,“O”手势对应寒冰魔法;这套手势系统在游戏中运行得非常顺畅。
为了帮助关卡设计师更快地制作关卡,我创建了多种工具来可视化游戏元素。
移动平台系统 是 2D 平台游戏的基础系统之一。实现难点包括:如何让角色随平台一起移动,以及如何判断角色是否站在平台上。
对于第一个问题,我调整了角色的 Rigidbody:不再设置角色速度,而是手动改变角色位置,使其跟随平台。第二个问题则通过 Raycast 解决,同时也处理了两个平台贴合时的判定问题。