Using Apple’s GameKit APIs with Godot¶
This is a quick guide on using the APIs in this Godot addon to access Apple’s GameKit APIs. For an overview of what you can do with GameKit, check [Apple’s GameKit Documentation](https://developer.apple.com/documentation/gamekit/)
One of the design choices in this binding has been to surface the same class names that Apple uses for their own data types to simplify looking things up and finding resources online. The method names on the other hand reflect the Godot naming scheme.
So instead of calling loadPhoto on GKPlayer, you would use the
load_photo method. And instead of the property gamePlayerID, you
would access game_player_id.
Table of Contents¶
Installation¶
Installing in your project¶
Make sure that you have added the directory containing the
“GodotApplePlugins” to your project, it should contain both a
godot_apple_plugins.gdextension and a bin directory with the
native libraries that you will use.
The APIs have been exposed to both MacOS and iOS, so you can iterate quickly on your projects.
Entitlements¶
For your software to be able to use the GameKit APIs, you will need
your Godot engine to have the com.apple.developer.game-center
entitlements. The easiest way to do this is to use Xcode to add the
entitlement to your iOS project.
See the file Entitlements for additional directions * without this, calling the APIs won’t do much.
Authentication¶
Create an instance of GameCenterManager, and then you can connect to
the authentication_error and authentication_result signals to
track the authentication state.
Then call the authenticate() method to trigger the authentication:
var game_center: GameCenterManager
func _ready() -> void:
game_center = GameCenterManager.new()
game_center.authentication_error.connect(func(error: String) -> void:
print("Received error %s" % error)
)
game_center.authentication_result.connect(func(status: bool) -> void:
print("Authentication updated, status: %s" % status
)
game_center.authenticate()
Players¶
Fetch the Local Player¶
From the GameCenterManager instance, you can access local_player, which is
a GKLocalPlayer. GKLocalPlayer is a subclass of GKPlayer and represents
the player using your game, with properties that track the local player state.
var local: GKLocalPlayer
func _ready() -> void:
gameCenter = GameCenterManager.new()
local = gameCenter.local_player
print("ONREADY: local, is auth: %s" % local.is_authenticated)
print("ONREADY: local, player ID: %s" % local.game_player_id)
There are a number of interesting properties in local_player that you
might want to use in your game like is_authenticated, is_underage,
is_multiplayer_gaming_restricted and so on.
GKPlayer¶
This is the base class for a player, either the local one or friends and contains properties and methods that are common to both
Apple Documentation:
Loading a Player Photo¶
# Here, we put the image inside an existing TextureRect, named $texture_rect:
local_player.load_photo(true, func(image: Image, error: Variant)->void:
if error == null:
$texture_rect.texture = ImageTexture.create_from_image(image)
)
Friends¶
# Loads the local player's friends list if the local player and their friends grant access.
local_player.load_friends(func(friends: Array[GKPlayer], error: Variant)->void:
if error:
print(error)
else:
for friend in friends:
print(friend.display_name)
)
# Loads players to whom the local player can issue a challenge.
local_player.local.load_challengeable_friends(func(friends: Array[GKPlayer], error: Variant)->void:
if error:
print(error)
else:
for friend in friends:
print(friend.display_name)
)
# Loads players from the friends list or players that recently participated in a game with the local player.
local_player.load_recent_friends(func(friends: Array[GKPlayer], error: Variant)->void:
if error:
print(error)
else:
for friend in friends:
print(friend.display_name)
)
FetchItemsForIdentityVerificationSignature¶
local_player.fetch_items_for_identity_verification_signature(func(values: Dictionary, error: Variant)->void:
if error:
print(error)
else:
print("Identity dictionary")
print(values)
)
Saved Games¶
Handling Conflicts¶
When multiple devices save games with the same name, conflicts can occur. You can handle these conflicts by registering a listener and implementing the resolution logic.
var game_center: GameCenterManager
var local: GKLocalPlayer
func _ready() -> void:
game_center = GameCenterManager.new()
local = game_center.local_player
# Register the listener to receive conflict signals
local.register_listener()
local.conflicting_saved_games.connect(_on_conflicting_saved_games)
func _on_conflicting_saved_games(player: GKPlayer, conflicts: Array) -> void:
print("Received conflict for player: %s" % player.alias)
# Logic to determine which data to keep (e.g., newest, highest score, or user choice)
# For this example, we assume we want to keep the data from the first conflicting save.
var chosen_save = conflicts[0] as GKSavedGame
chosen_save.load_data(func(data: PackedByteArray, error: Variant) -> void:
if error:
print("Error loading data: %s" % error)
return
# Resolve the conflict using the chosen data
local.resolve_conflicting_saved_games(conflicts, data, func(saved_games: Array[GKSavedGame], error: Variant) -> void:
if error:
print("Error resolving conflict: %s" % error)
else:
print("Conflict resolved!")
)
)
Saved Game Modifications¶
You can also listen for modifications to saved games (e.g., from other devices).
func _ready() -> void:
# ... setup local player ...
local.saved_game_modified.connect(_on_saved_game_modified)
func _on_saved_game_modified(player: GKPlayer, saved_game: GKSavedGame) -> void:
print("Saved game modified: %s" % saved_game.name)
# Reload data or update UI
Achievements¶
List all achievements¶
Note: This only returns achievements with progress that the player has reported. Use GKAchievementDescription for a list of all available achievements.
GKAchievement.load_achievements(func(achievements: Array[GKAchievement], error: Variant)->void:
if error:
print("Load achievement error %s" % error)
else:
for achievement in achievements:
print("Achievement: %s" % achievement.identifier)
)
List Descriptions¶
GKAchievementDescription.load_achievement_descriptions(func(adescs: Array[GKAchievementDescription], error: Variant)->void:
if error:
print("Load achievement description error %s" % error)
else:
for adesc in adescs:
print("Achievement Description ID: %s" % adesc.identifier)
print(" Unachieved: %s" % adesc.unachieved_description)
print(" Achieved: %s" % adesc.achieved_description)
)
Load Achievement Description Image¶
adesc.load_image(func(image: Image, error: Variant)->void:
if error == null:
$texture_rect.texture = ImageTexture.create_from_image(image)
else:
print("Error loading achievement image %s" % error)
Report Progress¶
Reporting Achievement First Time¶
var id = "a001"
var percentage = 100
GKAchievementDescription.load_achievement_descriptions(func(descriptions: Array[GKAchievementDescription], error: Variant)->void:
if error:
print("Load achievement descriptions error %s" % error)
else:
for desc in descriptions:
if desc.identifier == id:
var new_achievement := GKAchievement.new()
new_achievement.identifier = desc.identifier
new_achievement.percent_complete = percentage
GKAchievement.report_achievement([new_achievement], func(error: Variant)->void:
if error:
print("Error submitting achievement")
else:
print("Success!")
)
)
Updating Already Reported Achievement¶
var id = "a001"
var percentage = 100
GKAchievement.load_achievements(func(achievements: Array[GKAchievement], error: Variant)->void:
if error:
print("Load achievement error %s" % error)
else:
for achievement in achievements:
if achievement.identifier == id:
if not achievement.is_completed:
achievement.percent_complete = percentage
achievement.show_completion_banner = true
GKAchievement.report_achievement([achievement], func(error: Variant)->void:
if error:
print("Error submitting achievement")
else:
print("Success!")
)
)
Reset All Achievements¶
GKAchievement.reset_achievements(func(error: Variant)->void:
if error:
print("Error resetting" % error)
else:
print("Success")
)
Realtime Matchmaking¶
Events¶
You can use the convenience request_match method after configuring your request,
and on your callback setup the game_match to track the various states of the match,
like this:
var req = GKMatchRequest.new()
req.max_players = 2
req.min_players = 1
req.invite_message = "Join me in a quest to fun"
GKMatchmakerViewController.request_match(req, func(game_match: GKMatch, error: Variant)->void:
if error:
print("Could not request a match %s" % error)
else:
print("Got a match!")
game_match.data_received.connect(func (data: PackedByteArray, from_player: GKPlayer)->void:
print("Received data from Player")
)
game_match.data_received_for_recipient_from_player.connect(func(data: PackedByteArray, for_recipient: GKPlayer, from_remote_player: GKPlayer)->void:
print("Received data from a player to another player")
)
gameMatch.did_fail_with_error.connect(func(error: String)->void:
print("Match failed with %s" % error)
)
gameMatch.should_reinvite_disconnected_player = (func(player: GKPlayer)->bool:
# We always reinvite
return true
)
gameMatch.player_changed.connect(func(player: GKPlayer, connected: bool)->void:
print("Status of player changed to %s" % connected)
)
)
Disconnect¶
game_match.disconnect()
Send to all¶
var data = "How do you do fellow kids".to_utf8_buffer()
game_match.send_data_to_all_players(data, GKMatch.SendDataMode.reliable)
Send to Players¶
game_match.send(array, [first_player, second_player], GKMatch.SendDataMode.reliable)
Rule-Based Matchmaking Properties (iOS/macOS 17.2+)¶
You can attach properties to GKMatchRequest to let Game Center use rule-based
matching data, and inspect those values from GKMatch once a match is created.
var request := GKMatchRequest.new()
request.min_players = 2
request.max_players = 2
request.queue_name = "ranked_duo"
request.properties = {
"region": "us-east",
"skill_bucket": 12,
"mode": "ranked"
}
# Keys can be a player's game_player_id (or GKPlayer object).
request.recipient_properties = {
friend_player.game_player_id: {"role": "support"}
}
On the resulting GKMatch:
print(game_match.properties)
print(game_match.player_properties) # Dictionary keyed by game_player_id
Invite Acceptance Event¶
GKLocalPlayer now emits an invite_accepted signal when an invite is accepted.
Use it to immediately resolve to a GKMatch.
var local := game_center.local_player
var matchmaker := GKMatchmaker.new()
func _ready() -> void:
local.register_listener()
local.invite_accepted.connect(func(player: GKPlayer, invite: GKInvite) -> void:
matchmaker.match_for_invite(invite, func(match: GKMatch, error: Variant) -> void:
if error:
print("Invite match error: %s" % error)
else:
print("Joined invited match")
)
)
Turn-Based Matchmaking¶
Use GKTurnBasedMatch for asynchronous turn-based sessions, and register a
GKLocalPlayer listener to receive turn-based events.
var local := game_center.local_player
func _ready() -> void:
local.register_listener()
local.turn_event_received.connect(func(player: GKPlayer, match: GKTurnBasedMatch, did_become_active: bool) -> void:
print("Turn event for match %s (active=%s)" % [match.match_id, did_become_active])
)
local.match_requested_with_other_players.connect(func(player: GKPlayer, recipients: Array) -> void:
print("Turn-based request with %d recipients" % recipients.size())
)
local.turn_based_match_ended.connect(func(player: GKPlayer, match: GKTurnBasedMatch) -> void:
print("Turn-based match ended: %s" % match.match_id)
)
Create/find and advance a turn:
var request := GKMatchRequest.new()
request.min_players = 2
request.max_players = 2
GKTurnBasedMatch.find(request, func(match: GKTurnBasedMatch, error: Variant) -> void:
if error:
print("Find turn-based match failed: %s" % error)
return
match.load_match_data(func(data: PackedByteArray, load_error: Variant) -> void:
if load_error:
print("Load match data error: %s" % load_error)
return
var updated_data := "next turn payload".to_utf8_buffer()
var next_participants: Array = match.participants
match.end_turn(
next_participants,
604800.0, # one week default timeout
updated_data,
func(end_error: Variant) -> void:
if end_error:
print("End turn error: %s" % end_error)
)
)
)
Exchange-related signals:
* exchange_received(player, exchange, match)
* exchange_canceled(player, exchange, match)
* exchange_completed(player, replies, match)
* player_wants_to_quit_match(player, match)
Turn-Based Matchmaker UI¶
You can present Apple’s built-in turn-based matchmaking UI and handle its delegate callbacks through signals.
var request := GKMatchRequest.new()
request.min_players = 2
request.max_players = 2
var controller := GKTurnBasedMatchmakerViewController.create_controller(request)
controller.did_find_match.connect(func(match: GKTurnBasedMatch) -> void:
print("Found turn-based match %s" % match.match_id)
)
controller.cancelled.connect(func(_: String) -> void:
print("Turn-based matchmaking cancelled")
)
controller.failed_with_error.connect(func(message: String) -> void:
print("Turn-based matchmaking failed: %s" % message)
)
controller.present()
Challenges¶
GKLocalPlayer now emits challenge-related signals once you call
register_listener().
local.challenge_received.connect(func(player: GKPlayer, challenge: GKChallenge) -> void:
print("Challenge from: %s" % (challenge.issuing_player.display_name if challenge.issuing_player else "unknown"))
print("Message: %s" % challenge.message)
)
local.challenge_completed.connect(func(player: GKPlayer, challenge: GKChallenge, friend_player: GKPlayer) -> void:
print("Challenge completed by %s" % friend_player.display_name)
)
Load currently received challenges:
GKChallenge.load_received_challenges(func(challenges: Array, error: Variant) -> void:
if error:
print("Load challenges error: %s" % error)
else:
for challenge in challenges:
print("Challenge type: %s state: %s" % [challenge.challenge_type, challenge.state])
)
Leaderboards¶
Report Score¶
GKLeaderboard.load_leaderboards(PackedStringArray(["MyLeaderboard"]), func(leaderboards: Array [GKLeaderboard], error: Variant)->void:
var score = 100
var context = 0
if error:
print("Error loading leaderboard %s" % error)
else:
leaderboards[0].submit_score(score, context, local, func(error: Variant)->void:
if error:
print("Error submitting leadeboard %s" % error)
)
)
Load Leaderboards¶
# Loads all leaderboards
GKLeaderboard.load_leaderboards(PackedStringArray(), func(leaderboards: Array [GKLeaderboard], error: Variant)->void:
if error:
print("Error loading leaderboards %s" % error)
else:
print("Got %s" % leaderboards)
)
# Load specific ones
GKLeaderboard.load_leaderboards(PackedStringArray(["My leaderboard"]), func(leaderboards: Array [GKLeaderboard], error: Variant)->void:
if error:
print("Error loading leaderboard %s" % error)
else:
print("Got %s" % leaderboards)
)