Trials of the Archmage

2019 年 9 月—2019 年 12 月|学校项目


项目信息

  • 团队规模:4 人
  • 类型:2D 平台跳跃 / 解谜游戏
  • 平台:Android
  • 引擎:Unity 5
  • 开发时间:2 个月
  • 下载

关于

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

GestureController 脚本负责处理玩家的手势输入,并与数据库比对以识别玩家所画的手势。


魔法施放


施法系统 是本作的亮点。玩家通过真实手势亲手施放魔法,而不是点击“技能按钮”。

施法系统由两部分组成。第一部分是选择对象:玩家需要点击想要交互的物体;第二部分是绘制手势:玩家画出特定手势来召唤魔法。

在实现手势系统时,我参考了 $P Point-Cloud Recognizer 进行点集识别,实际效果很好。最终设计中,“V”手势对应火焰魔法,“O”手势对应寒冰魔法;这套手势系统在游戏中运行得非常顺畅。


设计工具


为了帮助关卡设计师更快地制作关卡,我创建了多种工具来可视化游戏元素。

  • 红点与蓝点:红点表示物体可受火焰魔法影响,蓝点表示物体可受寒冰魔法影响。
  • 青色线条:表示移动平台的路径,帮助设计师快速理解平台的运动方式。
  • 手势可视化工具:用于测试手势的独立场景,主要服务于手势管理与优化;通过改进手势集合,可以提升游戏体验。

移动平台


移动平台系统 是 2D 平台游戏的基础系统之一。实现难点包括:如何让角色随平台一起移动,以及如何判断角色是否站在平台上。

对于第一个问题,我调整了角色的 Rigidbody:不再设置角色速度,而是手动改变角色位置,使其跟随平台。第二个问题则通过 Raycast 解决,同时也处理了两个平台贴合时的判定问题。


项目复盘


做得好的地方

  • 提前解决技术问题,并持续为新功能找到可行方案,使团队能够在不拖延进度或破坏游戏的前提下尝试新的创意与工具。
  • 合理安排开发与调试时间,保持时间上的灵活性,从而及时响应关卡设计师或美术的临时变更。
  • 通过添加粒子效果和 UI 动画,尽力提升游戏的美术表现。

需要改进的地方

  • 讨论问题时没有很好地控制情绪,表现得不够专业。
  • 执行个人任务时没有始终遵循 Scrum 看板;有时完成了看板之外的工作,却忘记将其补充到看板中。
  • 我没有充分尊重其他成员的工作。尽管每个人都在全力投入,我仍用更高标准要求大家、希望游戏变得更好,这在团队讨论中造成了一些误解与冲突。

我的收获

  • 尽量与团队计划保持一致;主动让队友了解自己的工作进度,是我的责任,也有助于制定更好的项目计划。
  • 学会尊重其他成员的工作,在成果完成时给予认可,并以礼貌的方式提出自己的想法和顾虑。
  • 与团队成员和利益相关者保持沟通非常重要。团队内部的误解会给项目带来摩擦,而与利益相关者的误解甚至可能导致项目失败。