Skip to content

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

cs
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);
    }

}
cs
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"));
    }
}
cs
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);
    }
}
cs
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;
    }
}
txt
*.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".

Alt text

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.