Chinese Character Game

Play the Chinese Character Game here.

Objectives of this project:

  • Finish my first project!
  • Get to grips with Unity
  • Improve my C# skills

When I was learning Chinese at SOAS, I didn’t really know how to get my characters to look not completely weird. How do they do the little box things quickly without making it a circle? Then I discovered that you don’t just draw a square; there’s a little thing called stroke order, once the basic rules of which are learnt makes writing and remembering characters much more intuitive. Now my Chinese handwriting (Korean handwriting too) is much more legible, even if my Chinese friend tells me I still write like a 10 year old.

Combining this with a nostalgic memory of the old Harry Potter shovelware games where you learned spells by tracing over the wand movement with your mouse, I thought this would be a good basis for a first “game” project, given the advice that to get started in games I should make something simple, finish it and move on. I built it in Unity given that it was free, simple and I could script in C#, which I’ve been most recently learning.

I took some code for the line rendering from the app guruz, but heavily modified it to get it to detect whether the line was completely inside the stroke and going in the right direction. My reference for the Chinese characters was the very comprehensive and useful dictionary at Arch Chinese, which as it happens has its own handy stroke order learning application.

Given that this was a short, programming focused project there is a lot that could be done to improve the final product, but I chose to (as advised) finish it and move on. If I were to make it a full game I would:

  • Allow the player to choose which character to learn, with characters unlocked by playing the “learn” mode once.
  • Store the player’s test scores for each character and show progress bars and levels.
  • Add a lot more characters.
  • Include voice clips of the pronunciation of each character.
  • Include a tutorial showing the basic stroke order rules, and reinforce each rule by learning a character which has a stroke order that can be deduced with this rule- eg. show the “horizontal before vertical” rule before learning the character 十.

Here is the code for the stroke class of the game:

using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using System.Collections.Generic;using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using System.Collections.Generic;

public class Stroke : MonoBehaviour {

public bool complete;
int mode; //0 for learning 1 for testing
Character character;
GameController gameController;
UIController UI;

//children and their components
PolygonCollider2D strokeCollider;
SpriteRenderer arrowRender;
BoxCollider2D startCollider;
BoxCollider2D endCollider;
public SpriteRenderer render;

//linerenderer variables
private LineRenderer line;
private bool isMousePressed;
public List<Vector2> pointsList = new List<Vector2>();
private Vector2 mousePos;
private Vector2 startPoint;
private Vector2 endPoint;
private bool success;
private int failures;
string layerName = “Line”;

//Initialisation on instantiation of character
void Awake()
{
//get character, ui and gamecontroller reference
GameObject characterObj = transform.parent.gameObject;
character = characterObj.GetComponent<Character>();
gameController = FindObjectOfType<GameController>();
UI = FindObjectOfType<UIController>();

//initialise linerenderer
line = gameObject.AddComponent<LineRenderer>();
line.material = gameController.defaultMat;
line.SetVertexCount(0);
line.SetWidth(0.2f, 0.2f);
line.SetColors(Color.white, Color.white);
line.useWorldSpace = true;
isMousePressed = false;
pointsList = new List<Vector2>();
line.sortingLayerName = layerName;

//collect the sprites and colliders associated with the stroke and store them in gameobject variables
strokeCollider = GetComponent<PolygonCollider2D>();
render = GetComponent<SpriteRenderer>();

foreach (Transform child in transform)
{
switch (child.gameObject.tag)
{
case “arrow”:
arrowRender = child.GetComponent<SpriteRenderer>();
break;
case “start collider”:
startCollider = child.GetComponent<BoxCollider2D>();
break;
case “end collider”:
endCollider = child.GetComponent<BoxCollider2D>();
break;
}
}
}

// Update is called once per frame
void Update ()
{
//MOUSE BUTTON DOWN ———-called when user presses mouse for the first time//
if (Input.GetMouseButtonDown(0))
{
if (IsClickInArea())
{
//disable arrow, hide any bubbles
isMousePressed = true;
arrowRender.enabled = false;
success = true;
UI.HideBubble();
}
}
//MOUSE BUTTON UP ————Called when user has finished drawing the line//
if (Input.GetMouseButtonUp(0))
{
if (pointsList.Count > 0)
{
isMousePressed = false;

//finds if end points overlap with start & end collider
startPoint = pointsList[0];
endPoint = pointsList[pointsList.Count – 1];

if (!startCollider.OverlapPoint(startPoint))
success = false;
if (!endCollider.OverlapPoint(endPoint))
success = false;

//checks if success or failure, shows UI response accordingly
if (success)
{
//SUCCESS//- button now sends user to next stroke
Next();
}
else
{
//FAILURE//
failures++;
character.score = character.score – character.scoreInc;
if (failures < 3)
{
//LESS THAN 3 FAILURES – instructions are repeated and user prompted to try again, line reset
if (mode == 0)
{
UI.ShowBubble(“Try again! Try and keep within the highlighted area and make sure you are drawing the stroke in the right direction.”);
}
else
{
UI.ShowBubble(“Try again!”);
}
line.SetVertexCount(0);
pointsList.RemoveRange(0, pointsList.Count);
}
else
{
//THIRD FAILIURE- button now sends user to next stroke
UI.ShowBubble(“Try the next stroke”);
Next();
}
}
}
}
//MOUSE IS HELD ————–Renders the line when the mouse is pressed down & checks each point is in the stroke//
if (isMousePressed)
{
mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
if (IsClickInArea(mousePos))
{
pointsList.Add(mousePos);
line.SetVertexCount(pointsList.Count);
line.SetPosition(pointsList.Count – 1, (Vector2)pointsList[pointsList.Count – 1]);
}
else
{
isMousePressed = false;
}
if (!strokeCollider.OverlapPoint(mousePos))
success = false;
}
}

//————————INITIATE STROKE————————//
public void InitStroke(int m)
{
//reset variables and give user instructions.
mode = m;
complete = false;
failures = 0;

switch (mode)
{
case 0:
//in learning mode, enable stroke highlight and arrow
render.enabled = true;
arrowRender.enabled = true;
break;
case 1:
//in testing mode, make sure arrow and highlight are hidden
render.enabled = false;
arrowRender.enabled = false;
break;
}
}

//———————–ON TO NEXT STROKE———————–//
public void Next()
{
//called once stroke has been successfully drawn or failed 3 times
//show finished stroke in red, clear line and flag stroke as complete
render.enabled = true;
Color col = new Color32 (255,0,255,255);
render.color = col;
line.SetVertexCount(0);
pointsList.RemoveRange(0, pointsList.Count);
complete = true;
}

bool IsClickInArea()
{
mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
return gameController.lineArea.OverlapPoint(mousePos);
}

bool IsClickInArea(Vector2 pos)
{
return gameController.lineArea.OverlapPoint(pos);
}
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s