Trials of the Archmage

September 2019 - December 2019 | School Project


Project Information

  • Team Size: 4
  • Genre: 2D Platformer / Puzzle Game
  • Platform: Android
  • Engine: Unity 5
  • Develop time: 2 months
  • Download

About

Trials of the Archmage is a fantasy game in which you solve puzzles with magic.​

Maeve the wizard wants one thing in this world: to take the title of greatest wizard of the age, an Archmage. To do so, instead of sitting around at her magic academy and being spoon-fed theories, she decides to trek through the Forlorn Forest and complete the "Trials of the Archmage", a series of mystical tests in which a wizard must demonstrate their mastery of fire and ice magic.


My contributions

  • The only programmer of the team, implemented all the gameplay functions.
  • Created a spell casting system for the game to create real magic feelings.

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

The GestureController is the script for handling user gesture inputs, also comparing it with the database to find the gesture user inputs.


Spell Casting


Spell Casting System is the highlight point of this game. With a real gesture input, players can cast magic with their hands instead of clicking "skill buttons".

The Spell Casting System consists of two parts. The first part is selecting object - Player needs to click on the object they wanted to interact with. The second part is drawing the gesture, where players can draw specific gestures to call the magic.

For implementing the Gesture System, I refered to $P Point-Cloud Recognizer for point set recognition, and it works out pretty well. In our final design, we use "V" gestures for fire magic, and "O" gestures for ice magic. The Gesture System works like a charm in our game.


Design Tools


For helping the Level Designers create the levels quicker, I created several tools for visuallizing the elements in our game.

  • The red dots and blue dots: Red dots means the object can be affected by fire magic, while the blue dots means the object can be affected by ice magic.
  • Cyan lines: These lines represent for the moving path of the moving platforms, so the designers can understand the movements of the platforms quickly.
  • Gesture Visuallizer: A single scene for gesture testings, this is set up mainly for gesture management and refine purposes, we can improve the game feel with a better gesture set.

Moving Platforms


Moving Platforms System is one of the basic systems for 2D platformers. The challenge of creating the moving platforms are - how to make the characters move with the moving platforms, and how to judge if the character is on the platform.

For the first question, I changed the rigidbody of the character. Instead of setting velocity for the character, I manually change the position of the character to follow with the platforms. For the second question, I use raycast to solve this problem, this also resolved the problem where two platforms are sticking together.


Post Mortem


What Went Well

  • Solve the technical problems ahead of schedule, always find a solution for developing new functions for the game. This gives our team a chance to try new ideas and tools without getting behind the schedule or ruining the game.
  • Schedule time for development and debugging well. This makes my time flexible, so I can respond to instant changes by level designers or artists.
  • Tried my best to improve the art/aesthetics of the game by placing particle effects and UI animations.

What Went Wrong

  • When discussing the problems, I didn’t control my temper well and feel not professional.
  • I didn’t always follow the scrum board while doing my own tasks. Sometimes I just did something that does not exist on the scrum board and forgot to put it on board.
  • I showed few respects to other member’s work, although everyone is putting their best into the game, I felt like putting higher standards on them and wished the game would be better. This causes some misunderstandings and conflicts during team discussions.

What I learned

  • Try to keep consistent with team schedules, it is my responsibility to let teammates know my working process and for better project plans.
  • Learned to respect other member’s work, applause their work when it’s done, and use a polite manner to bring out my thoughts and concerns for discussion.
  • Communicating with team members and stakeholders is important, misunderstanding with team members could cause the struggles and crunkles in project and misunderstanding with stakeholders may make the project a failure.