SuikAR

Project

A fun experimentation project for exploring the possibility of having a 3D Suika Game in an AR environment.

The project is based on an translating an existing game Suika Game to AR


Project Details

Team Size: 1

Year: 2023

Length: 2 days

Engine: Unity

Source Control: GitHub

Notable Work

Overview

  • Translated the core gameplay loop into 3D AR and made necessary adjustments to the mechanics.
    • Required three identical fruits to collide for a match.
    • Showed only the next fruit to spawn.
  • Worked with XRToolkit, specifically utilizing ARRaycasting and ARPlanes.
  • Implemented a robust, generic event dispatcher, adopting a more centralized approach to the observer pattern.
  • Adhered strictly to the SOLID principles whenever possible.
  • Developed a custom gravity controller to manage the gravitational pull affecting the fruit.

Gameloop

The basic gameplay loop involves combining two identical fruits to create a larger fruit. The game follows a Tetris-like approach, providing information and choices to the player in a similar fashion.

To adapt to the gameplay taking place in a 3D world using AR, various changes needed to be made to both the core gameplay and supporting systems.

Match-3: With improved control and precision over fruit placement, combining fruit became much easier without significant hassle. Requiring three fruit to collide created a more engaging experience in a 3D environment.

Gravitational Pull: In the original game, losing fruit wasn't possible, but in the AR setting, players could shoot fruit outside the gameplay boundaries. To address this, a custom gravity system was implemented, pulling fruit toward the center of a specified object.

Gravity

A custom gravity system was implemented that would act as a gravitational pull for bypassing fruit.

using UnityEngine;

namespace SuikAR.Systems
{
	public class GravitationalPull : MonoBehaviour
	{
		[SerializeField] private float gravitationalForce = 9.81f;
		[Tooltip("Gravity strength should be lower the closer the fruit's distance to this value")]
		[SerializeField] private float gravitationalDeadzone = 0.3f; 

		private void FixedUpdate()
		{
		  foreach (var collider in Physics.OverlapBox(transform.position, transform.localScale))
		  {
			collider.TryGetComponent(out Rigidbody rb);
			if (rb)
			{
			  Vector3 direction = transform.position - collider.transform.position;
			  float distance = direction.magnitude;

			  if (distance > gravitationalDeadzone)
			  {
				// Calculate gravitational force
				float forceMagnitude = gravitationalForce * rb.mass / Mathf.Pow(distance, 2);
				Vector3 force = direction.normalized * forceMagnitude;

				// Apply force to the object
				rb.AddForce(force);
			  }
			  else if (distance <= gravitationalDeadzone && distance > 0)
			  {
				// Calculate reduced force within the dead zone
				float forceMagnitude = gravitationalForce * rb.mass / Mathf.Pow(gravitationalDeadzone, 2);
				float distanceFactor = Mathf.InverseLerp(0, gravitationalDeadzone, distance);
				float reducedForceMagnitude = Mathf.Lerp(0, forceMagnitude, distanceFactor);
				Vector3 force = direction.normalized * reducedForceMagnitude;

				// Apply force to the object
				rb.AddForce(force);
			  }
			}
		  }
		}
	} 
}													
										

Events

A generic event dispatcher that is designed to allow classes to subscribe to notifications and receive data without requiring coupling with other classes. It offers a more centralized approach to the observer pattern.

using System;
using System.Collections.Generic;

namespace SuikAR.Events
{
	public static class EventManager
	{
		private static Dictionary eventDictionary = new Dictionary();

		public enum Event
		{
		   OnGameStarted,
		   OnFruitCombine,
		   OnFruitQueued,
		   OnScored,
		   OnNewHighscore
		}
		
		public static void Subscribe(Event eventType, Action listener)
		{
		   eventDictionary.TryAdd(eventType, null);
		   eventDictionary[eventType] = Delegate.Combine(eventDictionary[eventType], listener);
		}
		
		public static void Unsubscribe(Event eventType, Action listener)
		{
		   if (eventDictionary.ContainsKey(eventType))
		   {
		      eventDictionary[eventType] = Delegate.Remove(eventDictionary[eventType], listener);
		   }
		}

		public static void Invoke(Event eventType, T value)
		{
		   if (eventDictionary.TryGetValue(eventType, out Delegate eventDelegate))
		   {
			 (eventDelegate as Action)?.Invoke(value);
		   }
		}
	}
}									
										


Links