Getting Started with ArcanePad in Unity
Welcome to the Arcanepad Unity Tutorial! This guide will help you get started with creating Arcanepad games in Unity.
Starter Template
https://github.com/imvenx/unity-starter-template-arcanepad
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using ArcanepadSDK.Models;
using ArcanepadSDK;
using System;
public class ViewManager : MonoBehaviour
{
public GameObject playerPrefab;
public List<Player> players = new List<Player>();
public bool gameStarted { get; private set; }
public static bool isGamePaused = false;
public GameObject pausePanel;
async void Awake()
{
// INITIALIZE CLIENT
Arcane.Init();
// WAIT UNTIL CLIENT IS INITIALIZED
var initialState = await Arcane.ArcaneClientInitialized();
// CREATE A PLAYER FOR EACH GAMEPAD THAT WAS CONNECTED BEFORE INITIALIZATION
initialState.pads.ForEach(pad => createPlayer(pad));
// CREATE A PLAYER FOR EACH GAMEPAD THAT CONNECTS AFTER INITIALIZATION
Arcane.Msg.On(AEventName.IframePadConnect, new Action<IframePadConnectEvent>(createPlayerIfDontExist));
// DESTROY PLAYER ON GAMEPAD DISCONNECT
Arcane.Msg.On(AEventName.IframePadDisconnect, new Action<IframePadDisconnectEvent>(destroyPlayer));
Arcane.Msg.On(AEventName.PauseApp, new Action<PauseAppEvent>(pauseApp));
Arcane.Msg.On(AEventName.ResumeApp, new Action<ResumeAppEvent>(resumeApp));
}
void createPlayerIfDontExist(IframePadConnectEvent e)
{
var playerExists = players.Any(p => p.Pad.IframeId == e.iframeId);
if (playerExists) return;
var pad = new ArcanePad(deviceId: e.deviceId, internalId: e.internalId, iframeId: e.iframeId, isConnected: true, user: e.user);
createPlayer(pad);
}
void createPlayer(ArcanePad pad)
{
if (string.IsNullOrEmpty(pad.IframeId)) return;
GameObject newPlayer = Instantiate(playerPrefab, Vector3.zero, Quaternion.identity);
Player playerComponent = newPlayer.GetComponent<Player>();
playerComponent.Initialize(pad);
players.Add(playerComponent);
}
void destroyPlayer(IframePadDisconnectEvent e)
{
var player = players.FirstOrDefault(p => p.Pad.IframeId == e.IframeId);
if (player == null) Debug.LogError("Player not found to remove on disconnect");
player.Pad.Dispose();
players.Remove(player);
Destroy(player.gameObject);
}
void pauseApp(PauseAppEvent e)
{
Debug.Log("pause");
isGamePaused = true;
pausePanel.SetActive(true);
}
void resumeApp(ResumeAppEvent e)
{
Debug.Log("resume");
isGamePaused = false;
pausePanel.SetActive(false);
}
}
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using ArcanepadSDK.Models;
using ArcanepadSDK;
using System;
public class ViewManager : MonoBehaviour
{
public GameObject playerPrefab;
public List<Player> players = new List<Player>();
public bool gameStarted { get; private set; }
public static bool isGamePaused = false;
public GameObject pausePanel;
async void Awake()
{
// INITIALIZE CLIENT
Arcane.Init();
// WAIT UNTIL CLIENT IS INITIALIZED
var initialState = await Arcane.ArcaneClientInitialized();
// CREATE A PLAYER FOR EACH GAMEPAD THAT WAS CONNECTED BEFORE INITIALIZATION
initialState.pads.ForEach(pad => createPlayer(pad));
// CREATE A PLAYER FOR EACH GAMEPAD THAT CONNECTS AFTER INITIALIZATION
Arcane.Msg.On(AEventName.IframePadConnect, new Action<IframePadConnectEvent>(createPlayerIfDontExist));
// DESTROY PLAYER ON GAMEPAD DISCONNECT
Arcane.Msg.On(AEventName.IframePadDisconnect, new Action<IframePadDisconnectEvent>(destroyPlayer));
Arcane.Msg.On(AEventName.PauseApp, new Action<PauseAppEvent>(pauseApp));
Arcane.Msg.On(AEventName.ResumeApp, new Action<ResumeAppEvent>(resumeApp));
}
void createPlayerIfDontExist(IframePadConnectEvent e)
{
var playerExists = players.Any(p => p.Pad.IframeId == e.iframeId);
if (playerExists) return;
var pad = new ArcanePad(deviceId: e.deviceId, internalId: e.internalId, iframeId: e.iframeId, isConnected: true, user: e.user);
createPlayer(pad);
}
void createPlayer(ArcanePad pad)
{
if (string.IsNullOrEmpty(pad.IframeId)) return;
GameObject newPlayer = Instantiate(playerPrefab, Vector3.zero, Quaternion.identity);
Player playerComponent = newPlayer.GetComponent<Player>();
playerComponent.Initialize(pad);
players.Add(playerComponent);
}
void destroyPlayer(IframePadDisconnectEvent e)
{
var player = players.FirstOrDefault(p => p.Pad.IframeId == e.IframeId);
if (player == null) Debug.LogError("Player not found to remove on disconnect");
player.Pad.Dispose();
players.Remove(player);
Destroy(player.gameObject);
}
void pauseApp(PauseAppEvent e)
{
Debug.Log("pause");
isGamePaused = true;
pausePanel.SetActive(true);
}
void resumeApp(ResumeAppEvent e)
{
Debug.Log("resume");
isGamePaused = false;
pausePanel.SetActive(false);
}
}
using System;
using System.Reflection;
using ArcanepadSDK.Models;
using ArcanepadSDK.Types;
using Newtonsoft.Json;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class PadManager : MonoBehaviour
{
public Button AttackButton;
public Button CalibrateQuaternionButton;
public Button CalibratePointerTopLeftButton;
public Button CalibratePointerBottomRight;
public TextMeshProUGUI LogsText;
async void Awake()
{
// INITIALIZE LIBRARY
Arcane.Init(new ArcaneInitParams(deviceType: ArcaneDeviceType.pad, padOrientation: AOrientation.Portrait));
// WAIT UNTIL CLIENT IS INITIALIZED
await Arcane.ArcaneClientInitialized();
// ATTACK
AttackButton.onClick.AddListener(OnAttackButtonPress);
// LISTEN FOR AN EVENT SENT TO THIS PAD
Arcane.Msg.On("TakeDamage", new Action<TakeDamageEvent>(TakeDamage));
// CALIBRATE ROTATION
CalibrateQuaternionButton.onClick.AddListener(() => Arcane.Pad.CalibrateQuaternion());
// GET PAD POINTER DATA (POSTION X, Y WHERE THE PAD IS POINTING TO THE SCREEN)
Arcane.Pad.StartGetPointer();
Arcane.Pad.OnGetPointer((GetPointerEvent e) => LogsText.text = "Pointer: x:" + e.x + " | y: " + e.y);
// CALIBRATE POINTER TOP LEFT
CalibratePointerTopLeftButton.onClick.AddListener(() => Arcane.Pad.CalibratePointer(true));
// CALIBRATE POINTER BOTTOM RIGHT
CalibratePointerBottomRight.onClick.AddListener(() => Arcane.Pad.CalibratePointer(false));
}
void TakeDamage(TakeDamageEvent e)
{
// MAKE PAD VIBRATE 100 MS TIMES DAMAGE. IF DAMAGE IS 3 IT WILL VIBRATE 300 ms
Arcane.Pad.Vibrate(100 * e.damage);
}
void OnAttackButtonPress()
{
// EMIT EVENT FROM PAD TO VIEW
Arcane.Msg.EmitToViews(new ArcaneBaseEvent("Attack"));
}
}
using System;
using System.Reflection;
using ArcanepadSDK.Models;
using ArcanepadSDK.Types;
using Newtonsoft.Json;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class PadManager : MonoBehaviour
{
public Button AttackButton;
public Button CalibrateQuaternionButton;
public Button CalibratePointerTopLeftButton;
public Button CalibratePointerBottomRight;
public TextMeshProUGUI LogsText;
async void Awake()
{
// INITIALIZE LIBRARY
Arcane.Init(new ArcaneInitParams(deviceType: ArcaneDeviceType.pad, padOrientation: AOrientation.Portrait));
// WAIT UNTIL CLIENT IS INITIALIZED
await Arcane.ArcaneClientInitialized();
// ATTACK
AttackButton.onClick.AddListener(OnAttackButtonPress);
// LISTEN FOR AN EVENT SENT TO THIS PAD
Arcane.Msg.On("TakeDamage", new Action<TakeDamageEvent>(TakeDamage));
// CALIBRATE ROTATION
CalibrateQuaternionButton.onClick.AddListener(() => Arcane.Pad.CalibrateQuaternion());
// GET PAD POINTER DATA (POSTION X, Y WHERE THE PAD IS POINTING TO THE SCREEN)
Arcane.Pad.StartGetPointer();
Arcane.Pad.OnGetPointer((GetPointerEvent e) => LogsText.text = "Pointer: x:" + e.x + " | y: " + e.y);
// CALIBRATE POINTER TOP LEFT
CalibratePointerTopLeftButton.onClick.AddListener(() => Arcane.Pad.CalibratePointer(true));
// CALIBRATE POINTER BOTTOM RIGHT
CalibratePointerBottomRight.onClick.AddListener(() => Arcane.Pad.CalibratePointer(false));
}
void TakeDamage(TakeDamageEvent e)
{
// MAKE PAD VIBRATE 100 MS TIMES DAMAGE. IF DAMAGE IS 3 IT WILL VIBRATE 300 ms
Arcane.Pad.Vibrate(100 * e.damage);
}
void OnAttackButtonPress()
{
// EMIT EVENT FROM PAD TO VIEW
Arcane.Msg.EmitToViews(new ArcaneBaseEvent("Attack"));
}
}
using System;
using ArcanepadExample;
using ArcanepadSDK;
using ArcanepadSDK.Models;
using Newtonsoft.Json;
using UnityEngine;
public class Player : MonoBehaviour
{
public ArcanePad Pad { get; private set; }
public RectTransform pointer;
public void Initialize(ArcanePad pad)
{
Pad = pad;
// USER NAME AND COLOR
Debug.Log("User Name: " + Pad.User.name);
Debug.Log("User Color: #" + Pad.User.color);
// LISTEN EVENT SENT FROM PAD TO VIEW, IN DIFFERENT WAYS:
// Pad.On("Attack", (ArcaneBaseEvent e) => { }); // UNTYPED LAMBDA
// Pad.On("Attack", new Action<ArcaneBaseEvent>(Attack)); // UNTYPED FUNCTION
// Pad.On("Attack", (AttackEvent e) => { }); // TYPED LAMBDA
Pad.On("Attack", new Action<AttackEvent>(Attack)); // TYPED FUNCTION
Pad.StartGetQuaternion();
Pad.OnGetQuaternion(new Action<GetQuaternionEvent>(RotatePlayer));
// pad.StopGetQuaternion() // STOP
GameObject pointerObject = GameObject.Find("Pointer");
pointer = pointerObject.GetComponent<RectTransform>();
Pad.StartGetPointer();
Pad.OnGetPointer(new Action<GetPointerEvent>(MovePointer)); // FUNCTION
// Pad.OnGetPointer((GetPointerEvent e) => Debug.Log(e.x + " | " + e.y)); // LAMBDA
// Pad.StopGetPointer(); // STOP
// GET LINEAR ACCELERATION
// Pad.StartGetLinearAcceleration();
// Pad.OnGetLinearAcceleration((GetLinearAccelerationEvent e) => { Debug.Log("Linear Acceleration: " + JsonConvert.SerializeObject(e)); });
// Pad.OnGetLinearAcceleration(new Action<GetLinearAccelerationEvent>(OnGetLinearAcceleration)); // FUNCTION
// Pad.StopGetLinearAcceleration() // STOP
}
void Attack(ArcaneBaseEvent e)
{
if (ViewManager.isGamePaused) return;
Debug.Log(Pad.User.name + " has attacked");
// MAKE PAD VIBRATE FROM VIEW
// Pad.Vibrate(1000);
// EMIT EVENT TO PAD FROM VIEW
Pad.Emit(new TakeDamageEvent(3)); // TYPED
// Pad.Emit(new ArcaneBaseEvent("TakeDamage")); // UNTYPED (CAN'T DEFINE MEMBERS SO IT ONLY WORKS FOR EVENTS WITHOUT DATA)
}
void RotatePlayer(GetQuaternionEvent e)
{
if (ViewManager.isGamePaused) return;
transform.rotation = new Quaternion(e.x, e.y, e.z, e.w);
}
void MovePointer(GetPointerEvent e)
{
if (ViewManager.isGamePaused) return;
float normalizedX = e.x / 100f;
float normalizedY = -e.y / 100f;
RectTransform canvasRectTransform = pointer.parent as RectTransform;
float newX = normalizedX * canvasRectTransform.rect.width;
float newY = normalizedY * canvasRectTransform.rect.height;
pointer.anchoredPosition = new Vector2(newX, newY);
}
}
using System;
using ArcanepadExample;
using ArcanepadSDK;
using ArcanepadSDK.Models;
using Newtonsoft.Json;
using UnityEngine;
public class Player : MonoBehaviour
{
public ArcanePad Pad { get; private set; }
public RectTransform pointer;
public void Initialize(ArcanePad pad)
{
Pad = pad;
// USER NAME AND COLOR
Debug.Log("User Name: " + Pad.User.name);
Debug.Log("User Color: #" + Pad.User.color);
// LISTEN EVENT SENT FROM PAD TO VIEW, IN DIFFERENT WAYS:
// Pad.On("Attack", (ArcaneBaseEvent e) => { }); // UNTYPED LAMBDA
// Pad.On("Attack", new Action<ArcaneBaseEvent>(Attack)); // UNTYPED FUNCTION
// Pad.On("Attack", (AttackEvent e) => { }); // TYPED LAMBDA
Pad.On("Attack", new Action<AttackEvent>(Attack)); // TYPED FUNCTION
Pad.StartGetQuaternion();
Pad.OnGetQuaternion(new Action<GetQuaternionEvent>(RotatePlayer));
// pad.StopGetQuaternion() // STOP
GameObject pointerObject = GameObject.Find("Pointer");
pointer = pointerObject.GetComponent<RectTransform>();
Pad.StartGetPointer();
Pad.OnGetPointer(new Action<GetPointerEvent>(MovePointer)); // FUNCTION
// Pad.OnGetPointer((GetPointerEvent e) => Debug.Log(e.x + " | " + e.y)); // LAMBDA
// Pad.StopGetPointer(); // STOP
// GET LINEAR ACCELERATION
// Pad.StartGetLinearAcceleration();
// Pad.OnGetLinearAcceleration((GetLinearAccelerationEvent e) => { Debug.Log("Linear Acceleration: " + JsonConvert.SerializeObject(e)); });
// Pad.OnGetLinearAcceleration(new Action<GetLinearAccelerationEvent>(OnGetLinearAcceleration)); // FUNCTION
// Pad.StopGetLinearAcceleration() // STOP
}
void Attack(ArcaneBaseEvent e)
{
if (ViewManager.isGamePaused) return;
Debug.Log(Pad.User.name + " has attacked");
// MAKE PAD VIBRATE FROM VIEW
// Pad.Vibrate(1000);
// EMIT EVENT TO PAD FROM VIEW
Pad.Emit(new TakeDamageEvent(3)); // TYPED
// Pad.Emit(new ArcaneBaseEvent("TakeDamage")); // UNTYPED (CAN'T DEFINE MEMBERS SO IT ONLY WORKS FOR EVENTS WITHOUT DATA)
}
void RotatePlayer(GetQuaternionEvent e)
{
if (ViewManager.isGamePaused) return;
transform.rotation = new Quaternion(e.x, e.y, e.z, e.w);
}
void MovePointer(GetPointerEvent e)
{
if (ViewManager.isGamePaused) return;
float normalizedX = e.x / 100f;
float normalizedY = -e.y / 100f;
RectTransform canvasRectTransform = pointer.parent as RectTransform;
float newX = normalizedX * canvasRectTransform.rect.width;
float newY = normalizedY * canvasRectTransform.rect.height;
pointer.anchoredPosition = new Vector2(newX, newY);
}
}
using ArcanepadSDK.Models;
public class TakeDamageEvent : ArcaneBaseEvent
{
public int damage;
public TakeDamageEvent(int damage) : base("TakeDamage")
{
this.damage = damage;
}
}
using ArcanepadSDK.Models;
public class TakeDamageEvent : ArcaneBaseEvent
{
public int damage;
public TakeDamageEvent(int damage) : base("TakeDamage")
{
this.damage = damage;
}
}
*.log
*.dwlt
# Unity generated folders
[Tt]emp/
[Oo]bj/
[Bb]uild/
[Bb]uilds/
[Ll]ibrary/
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
*.opendb
*.VC.db
# Unity3D generated meta files
*.pidb.meta
*.bak
# OS generated
.DS_Store*
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
Logs/shadercompiler-UnityShaderCompiler.exe0.log
Logs/shadercompiler-UnityShaderCompiler.exe0.log
UserSettings/Layouts/default-2022.dwlt
*.log
*.dwlt
# Unity generated folders
[Tt]emp/
[Oo]bj/
[Bb]uild/
[Bb]uilds/
[Ll]ibrary/
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
*.opendb
*.VC.db
# Unity3D generated meta files
*.pidb.meta
*.bak
# OS generated
.DS_Store*
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
Logs/shadercompiler-UnityShaderCompiler.exe0.log
Logs/shadercompiler-UnityShaderCompiler.exe0.log
UserSettings/Layouts/default-2022.dwlt
Setup from Zero
If you want to start an Arcanepad Project in Unity from scratch you need to follow this steps, otherwise you can download the starter repo and skip this part.
1. Add Native Web Sockets package
Select "Add package by git url" and add this to the url
https://github.com/endel/NativeWebSocket.git#upm
https://github.com/endel/NativeWebSocket.git#upm
2. Add Newtonsoft package
Select "Add package by name" and write this on the name:
com.unity.nuget.newtonsoft-json
com.unity.nuget.newtonsoft-json
3. Get the SDK
Download the arcanepad-unity-sdk unity package from this link: https://github.com/imvenx/arcanepad-unity-sdk/releases and import it in your unity project
4. Set compression format to disabled
On Edit -> Project Settings -> Player -> WebGL settings -> Publishing Settings
set Comperssion format
to disabled
Creating View and Pad scenes
Handling Pad Connect/Disconnect
Emit Events Pad/View
Export and share your game
This is an alternative offline way to manually export and share our Arcanepad game. We working to add an easier way with UI menu to share and sell games online on the Arcanepad store, so anyone can discover it.
Don't use real time light on your gamepads!
Low end devices will struggle even with a single mesh if you use real time light, try to optimize as much as possible. Also you could use "hard shadows" or "no shadows".
Use coroutines instead of async on gamepads
Async and Await don't work well on webgl exports.
Upload your game to Arcanepad
Go to https://dev.arcanepad.com, create an account and after you are verified you can upload your game. The app folder has to be compressed on .zip format.