HeaderSIS.jpg

IS480 Team wiki: 2012T1 6P Development

From IS480
Jump to navigation Jump to search
6pcube.png Home   6pProject overviewicon.pngProject Overview   6pProject managementIcon.png Project Management   Project Docs.png Project Documentation   6pMeeting minutesIcon.png Meeting Minutes   6PGralch.png Game Development


  Overview   Game Mechanics   User Testing   UI Screenshots   Graphics Development   Technical Complexity  


Technical Complexity

Game Scenes Setup

Sprite collections with polygon colliders setup

Every images before appearing on the scene has to be added to a sprite collection library where its precise settings and parameters are manually set in order for the image to take shape in the scene and allow it to have natural interaction with other objects.

Collection1.pngCollection2.png Collection3.pngCollection4.png

Scene setup

The following is an example of a scene setup that consists of 16 different objects laid out in one frame of the scene. The average amount of objects in one stage would go up to about 400 and the crucial part of making the game fun and challenging greatly relies on the precise manual positioning of every objects in this step.

Scene1.pngScene2.png Scene3.pngScene4.png Scene5.pngScene6.png


Precise control of objects behaviour

The main focus of the development work of our project is to achieve precise control over the manipulation of the components' behaviour that are attached to all objects with the use of C# scripts, this includes collision and effects handling for every single event.

SoftwareDesign.png

With the use of C# scripts, we are able to have precise control on every collision events and the desired outcome of each case:

  1. Gralch triggering a trap at the precise contact point.
  2. Gralch gains x amount of energy upon contact with y type of cherry.
  3. Gralch loses x amount of energy upon contact with y type of obstacle.
  4. Gralch passing a checkpoint which result to a notification to user.
  5. Gralch movements along the game
    1. Elevates upon touch and hold of mobile input on touchscreen
    2. Freefall on release of touch of mobile input on touchscreen
  6. Gralch animations
    1. Idle - During start of game
    2. Flying - Upon exiting contact with platform or planks
    3. Running - Upon contact with platform or planks
  7. Constant draining of Gralch's energy throughout game.
  8. Gradual speed increase of Gralch throughout the stage as player progresses to further checkpoints.


Game Controls

Pseudo Code For Touch & Hold

IF( Mobile Touch Input )
 IF( GameHasStarted )
  play flying animation
  button pressed is true
  flying is true
 ELSE
  set rigidbody kinematic to false
  IF(Audio not mute)Start play audio
   GameHasStarted is true
   flying is true
   play flying animation
   add force to rigidbody

 IF(button pressed is true)
  if(current animation is "Running")
   play flying animation
  add force to rigid body

Pseudo Code For Swipe

IF (Input.GetButtonDown)
	initialize flickTime to 0
	get the current pixel position where the input is detected and store as posDown
	take note of the time when input is detected and store as timeDown
IF (Input.GetButtonUp)
	get the current pixel position where the input is last detected posUp
	take note of the time when input is last detected and store as timeUp
	update flickTime to be the difference between timeUp and timeDown
	update flickLength to be the distance between the posUp and posDown
	calculate flickSpeed by having distance, flickLength divide by time, flickTime
 	IF (flickSpeed is beyond limit, flickSpeedCap)
		set flickSpeed to flickSpeedCap
	
	convert both pixel positions, posDown and posUp to  the world position, 
	vectorDown and vectorUp
Calculate force to add to the object using this formula - (vectorDown - vectorUp) x flickSpeed
Add rigid body force to the object’s ForceMode property

Pseudo Code For Joystick

//Initialization Method
Initialize vector position of touchArea to be the bottom left of the screen
Get the center position of the touchArea to be touchAreaCenter (x + width/2, y + height/2)
//Linear Interpolation Method for the movement of joystick object
Creates an animation that smoothly interpolates through a range of data points
Takes in two data points, (xMin,yMin) and (xMax,yMax)
xVar in this case is the x position of the joystick object
Formula: y = yMin + (yMax – yMin) * ((xVar - xMin) / (xMax – xMin))
//Method to move joystick
Get the current pixel position where the input is detected within the touchArea to be posCurr
Get the difference in length between posCurr and touchAreaCenter and store it as posDiff
IF (Input.GetButtonDown)
	IF(posDiff is larger than the joystickRadius)
		lock joystick.x and joystick.y at the joystick’s circumference (joystickRadius * PI * 2)		
	Convert joystick pos (-1 to be left (yMin), 1 to be right (yMax)) using joystick circumference as a limit
	apply linear interpolation method to get horizontal property
	vertical property is basically an inverted output of horizontal position		
ELSE
	reset joystick.x and joystick.y position to be touchAreaCenter
	horizontal and vertical properties are equal to 0
Calculate the change in object’s position
upDown 
leftRight
Add rigid body force upDown to ForceMode.Velocity property
Add rigid body force, leftRight to ForceMode.Acceleration property

Frame rates issues

Use of performance profiler for tracking bottleneck and solving negative effects on frame rates

Framerate1.png

There are many components of the development that could cause massive frame rate drop issues, and it could be impossible to identify what are the causes while you playtest the game on the deployment device or even on the development machine, here is one example of how the use of performance profiler helps the team to narrow down the cause of frame rate drop. In this example, the massive frame rate drop is due to a set of codes used in the scripts attached to the objects in the scene.

Framerate2.png

Although, identification of the cause could help to speed up the process of solving the frame rate issues, it does not help to point out the actual bottleneck or inefficiency in the codes or other components, hence there is a need to trial and error and also seek help from the unity community to rectify this problem.

Google Analytics Integration with Unity Android

Our team decided that there is a need to do a large data collection for players' in game progress and playing styles in order to find out patterns or outliers that helps us to improve our game design and eventually the players' overall satisfaction level. We could only collect a limited amount of feedback from the public through email, ratings and feedback. Hence, we decided to do a data collection on a global scale using the free and powerful analytical tool by Google.

However, Google Analytics does not provide any methods or guide that allows data collection and tracking to be done on Unity3D Mobile platforms. Hence, we faced a great deal of difficulty of integrating this service into our project. After much research and experimentation, we managed to stumble upon a piece of custom script that was shared in unity forums by fellow Unity developers and we managed to edit the script accordingly to suit our needs.

Part of the implementation:

string a, b, c, d, e;
a = System.DateTime.Now.ToString("MM/dd");
b = System.DateTime.Now.ToString("HH:mm");
c = duration.Seconds.ToString();
d = GlobalVariables.levelCherriesCollected.ToString();
e = Mathf.RoundToInt(tPos.x).ToString();
 if (Application.internetReachability == NetworkReachability.ReachableViaLocalAreaNetwork || Application.internetReachability == NetworkReachability.ReachableViaCarrierDataNetwork){
  GoogleAnalyticsHelper.Settings("UA-xxxxxxxx-x", "http://analytics.6production.com"); // your id
  GoogleAnalyticsHelper.LogPage(Application.loadedLevel.ToString());
  GoogleAnalyticsHelper.LogEvent("Gralchv38","ProgressReport","GameOver",a+","+b+","+c+","+d+","+e+","+GlobalVariables.getTrapID(),0);
 } 

As you can see from above, we track players' progress, in terms of date and hour playing, duration they played, distance they went each game, number of cherries collected, last trap hit before game over.

Endless World Creation

The Endless World is the finalised game mechanics that the team focuses in the last stage of the project. In order to achieve such an effect of an endless world, there's a need to handle many of the different technical aspects:

  1. Differing themes - Transition between various scenes
  2. Differing obstacles - Randomisation of obstacles and cherries position
  3. Increasing difficulty and interactions - Dynamic obstacles
  4. Easy executable controls - Amplifying effects of control through the use of accelerometer

Transition between various scenes

Gralch Scene Transition.png

As gralch progresses through the scene, there’ll be a change in theme. However, the change must not be too sudden, or it will disrupt the user’s experience, and the flow of the game. Therefore, our group implemented a gradual change as Gralch progresses through the stages, by controlling the alpha (transparency) of the two backgrounds (overlay). Some additional points about the implementation are as shown below:

1. At the start of every segment (section of the map), the alpha of the theme image (to show) will be 1f, while the other image (theme) will be 0f.

2. As Gralch progresses along the segment, the alpha of the first theme will move towards 0f, while the other image will move towards 1f.

3. When Gralch reaches the next segment of the map, point 1 is repeated, this time with the other theme starting with 1f.

Example Code:

// find out which segment currently in
int segmentTravel = (int)((Runner.distanceTraveled % wholeInterval) / (wholeInterval / 4)) + 1;
// find out which round currently in
int roundTravel = (int)(Runner.distanceTraveled / wholeInterval) + 1;
// find out progress within segment
float progress = (Runner.distanceTraveled % (wholeInterval / 4)) / (wholeInterval / 4);
if (segmentTravel == 1){
 if(renderer.name.Contains("bg1")){
  renderer.material.color = new Color(renderer.material.color.r,renderer.material.color.g,renderer.material.color.b,1f);
 }
 if(renderer.name.Contains("bg2")){
  renderer.material.color = new Color(renderer.material.color.r,renderer.material.color.g,renderer.material.color.b,0f);
 }
}			
if (segmentTravel == 2){
 if(renderer.name == "foreground1"){
  renderer.material.color = new Color(((1 - progress)/2),renderer.material.color.g,renderer.material.color.b);
 }
 if(renderer.name.Contains("bg1")){
  renderer.material.color = new Color(renderer.material.color.r,renderer.material.color.g,renderer.material.color.b,1f - progress);
 }
 if(renderer.name.Contains("bg2")){
  renderer.material.color = new Color(renderer.material.color.r,renderer.material.color.g,renderer.material.color.b,progress);
 }
}
if (segmentTravel == 3){
 if(renderer.name.Contains("bg1")){
  renderer.material.color = new Color(renderer.material.color.r,renderer.material.color.g,renderer.material.color.b,0f);
 }
 if(renderer.name.Contains("bg2")){
  renderer.material.color = new Color(renderer.material.color.r,renderer.material.color.g,renderer.material.color.b,1f);
 }
}
if (segmentTravel == 4){
 if(renderer.name == "foreground1"){
  renderer.material.color = new Color(((progress)/2),renderer.material.color.g,renderer.material.color.b);
 }
 if(renderer.name.Contains("bg1")){
  renderer.material.color = new Color(renderer.material.color.r,renderer.material.color.g,renderer.material.color.b,progress);
 }
 if(renderer.name.Contains("bg2")){
  renderer.material.color = new Color(renderer.material.color.r,renderer.material.color.g,renderer.material.color.b,1f - progress);
 }
}

Randomisation of obstacles and cherries position

Some examples of the different obstacles:

1.png

2.png

The randomisation algorithm behind that is working behind the scene that changes these obstacles:

void Initialize () {
		instance = this;
		obArray = new List<Transform>();
		varOffset = 2f;
		for(int i = 0; i < prefabs.GetLength(0) ; i++){
			obArray.Add((Transform)Instantiate(prefabs[i], new Vector3(-10f, prefabs[i].position.y, -100f), Quaternion.identity));
		}
		initialized = true;
		enabled = true;
	}
	void Update () {
		if(!initialized){
			Initialize();
			ObstacleManager.generateObstacle(new Vector3(Runner.tPos.x+varOffset,0f,0f));
			return;
		}
		//float xPos =0;
		if(ob2 != null){
			Transform currentObj = ob2.FindChild("lastPosIndicator");
			
			if(currentObj.position.x + recycleOffset+ varOffset < Runner.tPos.x){
				ObstacleManager.generateObstacle(new Vector3(Runner.tPos.x+varOffset,0f,0f));
			}
		}		
	}
	public static void generateObstacle(Vector3 position){
		instance.worldPlacement = position;
		instance.worldPlacement.x += 6f;
		//Debug.Log("ArrayCount:" +instance.obArray.Count);
		
		int index = Random.Range(0,instance.obArray.Count);
		Transform ob = instance.obArray[index];

		for(int i=0;i< ob.childCount;i++){
			ob.GetChild(i).gameObject.active = true;
			ob.GetChild(i).gameObject.SetActiveRecursively(true);
		}
		ob.localPosition = new Vector3(instance.worldPlacement.x,ob.position.y,instance.worldPlacement.z); 
		ob.gameObject.active = true;
		instance.worldPlacement.x += 12f;
		
		int index2 = Random.Range(0,instance.obArray.Count);
		
		while(index2 == index){
			index2 =  Random.Range(0,instance.obArray.Count);
		}
		instance.ob2 = instance.obArray[index2];
		for(int i=0;i< instance.ob2.childCount;i++){
			instance.ob2.GetChild(i).gameObject.active = true;
			instance.ob2.GetChild(i).gameObject.SetActiveRecursively(true);
		}
		instance.ob2.localPosition = new Vector3(instance.worldPlacement.x,instance.ob2.position.y,instance.worldPlacement.z);
		instance.ob2.gameObject.active = true;
		
		int onOff = Random.Range(0,2);
		if(onOff == 1){
			DynamicObstacleManager.generateDynamicObstacle(instance.worldPlacement);
			 instance.varOffset = 3f;	
		}

Dynamic obstacles

Ghost1.png

This is the logic implementation of how the ghost indicator heads out and follows gralch as it flies up and down the world, we have to make use of enumerator and pause function to achieve the effects of the ghost following the gralch up and down as he flies.

	public static void showGhost(){
		instance.transform.position = new Vector3(Runner.tPos.x + 5.1f, Runner.tPos.y, Runner.tPos.z);
		instance.gameObject.active = true;
		FlyingGhost.alive = true;
		instance.StartCoroutine(instance.rushToGralch());
	}
	
	IEnumerator rushToGralch(){
		yield return new WaitForSeconds(2f);
		Vector3 currentPos = transform.position;
		transform.position = new Vector3(transform.position.x + 1f, transform.position.y, transform.position.z);
		Transform flyingGhost = obArray[0];
		flyingGhost.localPosition = currentPos;
		flyingGhost.gameObject.active=true;
		gameObject.active = false;
	}


Ghost2.png

Here is the logic implementation of how a ghost spirit is generated and flies towards gralch once the position is locked, here we see that we have to play with the rotation and position of of the ghost object to achieve the effect of a flying ghost that launches out.

public static void generateDynamicObstacle(Vector3 position){

		Vector3 worldPlacement = position;
		worldPlacement.x += 10f;
		int ran = Random.Range(0,instance.obArray.Count);
		Transform ob = instance.obArray[ran];

		for(int i=0;i< ob.childCount;i++){
				ob.GetChild(i).gameObject.active = true;
				ob.GetChild(i).gameObject.SetActiveRecursively(true);
			}
		ob.rigidbody.constraints = RigidbodyConstraints.None;
		ob.rigidbody.constraints = RigidbodyConstraints.FreezeRotationZ;
		ob.position = new Vector3(worldPlacement.x,ob.position.y,worldPlacement.z); 
		ob.rigidbody.constraints = RigidbodyConstraints.FreezePositionX|RigidbodyConstraints.FreezePositionY|RigidbodyConstraints.FreezePositionZ|RigidbodyConstraints.FreezeRotationZ;
		ob.gameObject.active = true;
	}

Amplifying effects of control through the use of accelerometer

This is the code implementation of the use of precise detection of shakes control by users, the threshold and optimal acceleration of shake has to be tested through multiple trial and error on many different deployment devices, finding such optimal shake acceleration and threshold is tedious:

	void FixedUpdate(){
		if(alive){
			transform.position = new Vector3(transform.position.x-0.05f,transform.position.y,transform.position.z);
		}
	}    

       void Update() {
		acceleration = Input.acceleration;
               lowPassValue = Vector3.Lerp(lowPassValue, acceleration, (1.0f/60.0f)/1.0f);
               deltaAcceleration = acceleration - lowPassValue;
    	if (deltaAcceleration.sqrMagnitude >= shakeDetectionThreshold && Runner.trapped)
    	{
        // Perform your "shaking actions" here, with suitable guards in the if check above, if necessary to not, to not fire again if they're already being performed.
	 		Handheld.Vibrate ();
	 		collider.isTrigger = true;
	 		alive=true;
	 		Runner.unfreeze(false);
	 		StartCoroutine(stopGhost());
   	 }

	IEnumerator stopGhost(){
		yield return new WaitForSeconds(2f);
		collider.isTrigger = false;
		alive = false;
 	}