Getting Started With Arcanepad on Godot 4
Welcome to the Arcanepad Godot Tutorial! This guide will help you get started with creating Arcanepad games in Godot.
WARNING
IMPORTANT!
We are using Godot 4.3.rc3. Don't try with older versions as they won't work. You can download it here https://godotengine.org/article/release-candidate-godot-4-3-rc-3/#downloads
On the video tutorial using 4.3.dev4, but it had a sound bug that the new rc3 has fixed. Also in the video we explain how to install export templates from the provided zip, this is no longer necesary as rc3 can install the export templates automatically on the export window. You just need to point to the downloaded zip's. Reach us at arcanepad@gmail.com or discord if you need help.
Repo
https://github.com/imvenx/arcanepad-godot-sdk
extends Control
const PadScenePath = "res://Scenes/Pad/Pad.tscn"
const ViewScenePath = "res://Scenes/View/View.tscn"
func _ready():
call_deferred("goToPropperScene")
# INFO: This is a trick we do to have automatically loaded the game pad in the web exports
# The way it works is: We set the export WebGL without the View scene, so when we load on our
# phone, it will go automatically to the Pad scene, but on the editor, since it has all the
# scenes it will load the View. Feel free to change it to fit your workflow.
func goToPropperScene():
if ResourceLoader.exists(PadScenePath) and not ResourceLoader.exists(ViewScenePath):
goToPadScene()
return
goToViewScene()
func goToPadScene():
get_tree().change_scene_to_file(PadScenePath)
func goToViewScene():
get_tree().change_scene_to_file(ViewScenePath)
extends Control
const PadScenePath = "res://Scenes/Pad/Pad.tscn"
const ViewScenePath = "res://Scenes/View/View.tscn"
func _ready():
call_deferred("goToPropperScene")
# INFO: This is a trick we do to have automatically loaded the game pad in the web exports
# The way it works is: We set the export WebGL without the View scene, so when we load on our
# phone, it will go automatically to the Pad scene, but on the editor, since it has all the
# scenes it will load the View. Feel free to change it to fit your workflow.
func goToPropperScene():
if ResourceLoader.exists(PadScenePath) and not ResourceLoader.exists(ViewScenePath):
goToPadScene()
return
goToViewScene()
func goToPadScene():
get_tree().change_scene_to_file(PadScenePath)
func goToViewScene():
get_tree().change_scene_to_file(ViewScenePath)
extends Node
# INFO: This is an implementation of how you could handle your gamepads
# connection/disconection, but is not definitive, if your game requires
# a different implementation go ahead and change this. For example maybe
# you don't want to destroy players on gamepad disconnect, instead you may
# want to pause the game.
var players := []
var gameStarted := false
var isGamePaused := false
var playerScene = preload("res://Scenes/View/Player/Player.tscn")
func _ready():
Arcane.init(self, { "deviceType": "view", "logLevel": "verbose" })
Arcane.msg.on(AEventName.ArcaneClientInitialized, onArcaneClientInitialized)
func onArcaneClientInitialized(initialState: AModels.InitialState):
# Create a player for each gamepad that existed before the view was created
for pad in initialState.pads: createPlayer(pad)
# Listen to gamepads connecting after the view was created and create them if no exist
Arcane.msg.on(AEventName.IframePadConnect, onIframePadConnect)
# Listen for gamepads disconnecting and destroy the player
Arcane.msg.on(AEventName.IframePadDisconnect, onIframePadDisconnect)
# Listen pause app
Arcane.msg.on(AEventName.PauseApp, onPauseApp)
# Listen resume app
Arcane.msg.on(AEventName.ResumeApp, onResumeApp)
func onPauseApp():
$PausePanel.show()
func onResumeApp():
$PausePanel.hide()
# We create a player if it wasn't created before
func onIframePadConnect(e):
var playerExists = players.any(func(player): return player.pad.iframeId == e.iframeId)
if playerExists: return
var pad = ArcanePad.new(e.deviceId, e.internalId, e.iframeId, true, e.user)
createPlayer(pad)
func onIframePadDisconnect(event):
var disconnectedPlayer = null
for _player in players:
if _player.pad.iframeId == event.iframeId:
disconnectedPlayer = _player
break
if disconnectedPlayer == null:
push_error("Player not found to remove on disconnect")
return
destroy_player(disconnectedPlayer)
func createPlayer(pad):
if(AUtils.isNullOrEmpty(pad.iframeId)): return
var newPlayer = playerScene.instantiate()
print(newPlayer)
newPlayer.initialize(pad)
add_child(newPlayer)
players.append(newPlayer)
func destroy_player(player):
players.erase(player)
player.queue_free()
extends Node
# INFO: This is an implementation of how you could handle your gamepads
# connection/disconection, but is not definitive, if your game requires
# a different implementation go ahead and change this. For example maybe
# you don't want to destroy players on gamepad disconnect, instead you may
# want to pause the game.
var players := []
var gameStarted := false
var isGamePaused := false
var playerScene = preload("res://Scenes/View/Player/Player.tscn")
func _ready():
Arcane.init(self, { "deviceType": "view", "logLevel": "verbose" })
Arcane.msg.on(AEventName.ArcaneClientInitialized, onArcaneClientInitialized)
func onArcaneClientInitialized(initialState: AModels.InitialState):
# Create a player for each gamepad that existed before the view was created
for pad in initialState.pads: createPlayer(pad)
# Listen to gamepads connecting after the view was created and create them if no exist
Arcane.msg.on(AEventName.IframePadConnect, onIframePadConnect)
# Listen for gamepads disconnecting and destroy the player
Arcane.msg.on(AEventName.IframePadDisconnect, onIframePadDisconnect)
# Listen pause app
Arcane.msg.on(AEventName.PauseApp, onPauseApp)
# Listen resume app
Arcane.msg.on(AEventName.ResumeApp, onResumeApp)
func onPauseApp():
$PausePanel.show()
func onResumeApp():
$PausePanel.hide()
# We create a player if it wasn't created before
func onIframePadConnect(e):
var playerExists = players.any(func(player): return player.pad.iframeId == e.iframeId)
if playerExists: return
var pad = ArcanePad.new(e.deviceId, e.internalId, e.iframeId, true, e.user)
createPlayer(pad)
func onIframePadDisconnect(event):
var disconnectedPlayer = null
for _player in players:
if _player.pad.iframeId == event.iframeId:
disconnectedPlayer = _player
break
if disconnectedPlayer == null:
push_error("Player not found to remove on disconnect")
return
destroy_player(disconnectedPlayer)
func createPlayer(pad):
if(AUtils.isNullOrEmpty(pad.iframeId)): return
var newPlayer = playerScene.instantiate()
print(newPlayer)
newPlayer.initialize(pad)
add_child(newPlayer)
players.append(newPlayer)
func destroy_player(player):
players.erase(player)
player.queue_free()
extends Node
var pad:ArcanePad
var padQuaternion := Quaternion.IDENTITY
func initialize(_pad:ArcanePad) -> void:
pad = _pad
# Get User Name
print("User Name: ", pad.user.name)
# Get User Color
print("User Color: #", pad.user.color)
# Listen messages from the gamepad
pad.on("Attack", onAttack)
pad.on("CustomEvent", onCustomEvent)
pad.on("SomeEvent", func(): print("something"))
# Stop Listening
pad.off("SomeEvent")
# Send message from viewer to gamepad
pad.emit(AEvents.ArcaneBaseEvent.new("HelloFromView"))
# Get rotation quaternion of the gamepad
pad.startGetQuaternion()
pad.onGetQuaternion(onGetQuaternion)
# pad.stopGetQuaternion()
# Get pointer of the gamepad
pad.startGetPointer()
pad.onGetPointer(onGetPointer)
# pad.stopGetPointer()
# Get linear acceleration of the gamepad
#pad.startGetLinearAcceleration()
#pad.onGetLinearAcceleration(onGetLinearAcceleration)
#pad.stopGetLinearAcceleration()
func _process(_delta):
# Rotate player mesh with gamepad rotation
$MeshInstance3D.transform.basis = Basis(padQuaternion)
func onAttack():
# Send message to pad
pad.emit(AEvents.ArcaneBaseEvent.new("HelloFromView"))
AUtils.writeToScreen(self, pad.user.name + " attacked")
func onCustomEvent(e):
prints("Received custom event with val: ", e.someVal)
func onGetQuaternion(e):
if(e.w == null || e.x == null || e.y == null || e.z == null): return
# Set gamepad rotation to our padQuaternion variable, which will be used on _process
# to rotate the mesh
padQuaternion.x = -e.x
padQuaternion.y = -e.y
padQuaternion.z = e.z
padQuaternion.w = e.w
func onGetPointer(e):
if(e.x == null || e.y == null): return
# Move the pointer
var viewport_size = get_viewport().get_size()
var new_x = viewport_size.x * (e.x / 100.0)
var new_y = viewport_size.y * (e.y / 100.0)
$Pointer.position = Vector2(new_x, new_y)
func onGetLinearAcceleration(e):
prints("Linear Acceleration: ", e)
extends Node
var pad:ArcanePad
var padQuaternion := Quaternion.IDENTITY
func initialize(_pad:ArcanePad) -> void:
pad = _pad
# Get User Name
print("User Name: ", pad.user.name)
# Get User Color
print("User Color: #", pad.user.color)
# Listen messages from the gamepad
pad.on("Attack", onAttack)
pad.on("CustomEvent", onCustomEvent)
pad.on("SomeEvent", func(): print("something"))
# Stop Listening
pad.off("SomeEvent")
# Send message from viewer to gamepad
pad.emit(AEvents.ArcaneBaseEvent.new("HelloFromView"))
# Get rotation quaternion of the gamepad
pad.startGetQuaternion()
pad.onGetQuaternion(onGetQuaternion)
# pad.stopGetQuaternion()
# Get pointer of the gamepad
pad.startGetPointer()
pad.onGetPointer(onGetPointer)
# pad.stopGetPointer()
# Get linear acceleration of the gamepad
#pad.startGetLinearAcceleration()
#pad.onGetLinearAcceleration(onGetLinearAcceleration)
#pad.stopGetLinearAcceleration()
func _process(_delta):
# Rotate player mesh with gamepad rotation
$MeshInstance3D.transform.basis = Basis(padQuaternion)
func onAttack():
# Send message to pad
pad.emit(AEvents.ArcaneBaseEvent.new("HelloFromView"))
AUtils.writeToScreen(self, pad.user.name + " attacked")
func onCustomEvent(e):
prints("Received custom event with val: ", e.someVal)
func onGetQuaternion(e):
if(e.w == null || e.x == null || e.y == null || e.z == null): return
# Set gamepad rotation to our padQuaternion variable, which will be used on _process
# to rotate the mesh
padQuaternion.x = -e.x
padQuaternion.y = -e.y
padQuaternion.z = e.z
padQuaternion.w = e.w
func onGetPointer(e):
if(e.x == null || e.y == null): return
# Move the pointer
var viewport_size = get_viewport().get_size()
var new_x = viewport_size.x * (e.x / 100.0)
var new_y = viewport_size.y * (e.y / 100.0)
$Pointer.position = Vector2(new_x, new_y)
func onGetLinearAcceleration(e):
prints("Linear Acceleration: ", e)
extends Control
func _ready():
Arcane.init(self, {"deviceType": "pad", "padOrientation": "portrait"})
Arcane.msg.on(AEventName.ArcaneClientInitialized, onArcaneClientInitialized)
# Listen event
Arcane.msg.on("HelloFromView", onHelloFromView)
func onArcaneClientInitialized(initialState: AModels.InitialState):
print(initialState.pads)
AUtils.writeToScreen(self, "Arcane Client Initialized")
# Send untyped Message from gamepad to viewer
Arcane.msg.emitToViews(AEvents.ArcaneBaseEvent.new("HelloFromPad"))
# Send typed event from gamepad to the viewer
Arcane.msg.emitToViews(MyCustomEvent.new(5))
func onHelloFromView():
AUtils.writeToScreen(self, "View Says Hello")
Arcane.pad.vibrate(200)
# INFO: Your custom typed events inherit from ArcaneBaseEvent.
# This could be an attack event or jump or move, etc.
class MyCustomEvent extends AEvents.ArcaneBaseEvent:
var someVal:int
func _init(_someVal:int):
super._init("MyCustomEvent")
someVal = _someVal
extends Control
func _ready():
Arcane.init(self, {"deviceType": "pad", "padOrientation": "portrait"})
Arcane.msg.on(AEventName.ArcaneClientInitialized, onArcaneClientInitialized)
# Listen event
Arcane.msg.on("HelloFromView", onHelloFromView)
func onArcaneClientInitialized(initialState: AModels.InitialState):
print(initialState.pads)
AUtils.writeToScreen(self, "Arcane Client Initialized")
# Send untyped Message from gamepad to viewer
Arcane.msg.emitToViews(AEvents.ArcaneBaseEvent.new("HelloFromPad"))
# Send typed event from gamepad to the viewer
Arcane.msg.emitToViews(MyCustomEvent.new(5))
func onHelloFromView():
AUtils.writeToScreen(self, "View Says Hello")
Arcane.pad.vibrate(200)
# INFO: Your custom typed events inherit from ArcaneBaseEvent.
# This could be an attack event or jump or move, etc.
class MyCustomEvent extends AEvents.ArcaneBaseEvent:
var someVal:int
func _init(_someVal:int):
super._init("MyCustomEvent")
someVal = _someVal
extends Button
func _ready():
# Send Event to views on attack button pressed
connect("pressed", func(): Arcane.msg.emitToViews(AEvents.ArcaneBaseEvent.new("Attack")))
extends Button
func _ready():
# Send Event to views on attack button pressed
connect("pressed", func(): Arcane.msg.emitToViews(AEvents.ArcaneBaseEvent.new("Attack")))
extends Button
func _ready():
connect("pressed", func(): Arcane.pad.calibratePointer(true))
extends Button
func _ready():
connect("pressed", func(): Arcane.pad.calibratePointer(true))
extends Button
func _ready():
connect("pressed", func(): Arcane.pad.calibratePointer(false))
extends Button
func _ready():
connect("pressed", func(): Arcane.pad.calibratePointer(false))
extends Button
func _ready():
connect("pressed", func(): Arcane.pad.calibrateQuaternion())
extends Button
func _ready():
connect("pressed", func(): Arcane.pad.calibrateQuaternion())
Downloading the starter template and exploring Main scene logic
Exploring the Gamepad and View scenes
Export game and add it to games library (optional)
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.
Setup from zero or update arcane library
If you want to start your project from zero or update the arcane library, you can download the latest version here https://github.com/imvenx/arcanepad-godot-sdk/releases