Skip to content

Instantly share code, notes, and snippets.

@DrewFitz
Last active April 29, 2024 09:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save DrewFitz/0c84edfda7ed195f367f0162f15d4c9b to your computer and use it in GitHub Desktop.
Save DrewFitz/0c84edfda7ed195f367f0162f15d4c9b to your computer and use it in GitHub Desktop.
SharePlay in Unity
Assets
-> Scripts
-> PluginHelper.cs and other game-specific code
-> Plugins
-> iOS
-> UnityIosPlugin
-> Editor
-> SwiftPostProcess.cs
-> Source
-> SharePlayPlugin.h
-> SharePlayPlugin.m
-> GroupActivityStuff.swift
//
// GroupActivityStuff.swift
// GroupActivityStuff
//
// Created by Drew Fitzpatrick on 7/29/21.
//
import GroupActivities
import Combine
import Foundation
// MARK: - Activity
struct MyActivity: GroupActivity {
let ownerID: UUID
var metadata: GroupActivityMetadata {
get {
var meta = GroupActivityMetadata()
meta.title = "MyActivity"
meta.type = .generic
return meta
}
}
}
// MARK: - Messages
struct SetupMessage: Codable {
var hostParticipantID: UUID
}
struct PlayerNamesMessage: Codable {
var names: [UUID : String]
}
struct RegisteredPlayersMessage: Codable {
var playerIDs: [UUID]
}
struct GameStartMessage: Codable { }
struct SelectedPromptMessage: Codable {
var promptIndex: Int
}
struct SelectedSoundMessage: Codable {
var sound1Index: Int
var sound2Index: Int
}
struct ShowResolveStepMessage: Codable { }
struct PickWinnerMessage: Codable {
var winnerIndex: Int
}
struct NewRoundMessage: Codable { }
struct NewLeaderMessage: Codable {
let newLeaderID: UUID
}
@objc public class MyActivityController: NSObject {
private let names = [
"Ada Lovelace",
"Steve Jobs",
"The Woz",
"Dr. Zoidberg",
"Marie Curie",
"Nikola Tesla",
"Thomas Edison"
]
@objc public static var shared = MyActivityController()
// MARK: - Private SharePlay State
private var sessionTask: Task<Void, Never>?
private var tasks = [Task<Void, Never>]()
private var subscriptions = [AnyCancellable]()
private var messenger: GroupSessionMessenger?
private var currentSession: GroupSession<MyActivity>?
// MARK: - Public Game State
public var hostParticipantID: UUID?
public var leaderParticipantID: UUID?
public var playerChoices = [UUID: (Int, Int)]()
public var playerNames = [UUID: String]()
private lazy var myActivityUUID = UUID()
@objc public var isLeader: Bool {
leaderParticipantID == localParticipant?.id
}
@objc public var isHost: Bool {
guard let localID = localParticipant?.id else { return false }
return hostParticipantID == localID
}
// MARK: - Private Game State
private var localParticipant: Participant? {
currentSession?.localParticipant
}
var registeredParticipants = [UUID]()
// MARK: - Public Functions
@objc public var startGameHandler: (() -> Void)?
@objc public var selectedPromptHandler: ((Int) -> Void)?
@objc public var showResolveHandler: (() -> Void)?
@objc public var pickWinnerHandler: ((Int) -> Void)?
@objc public var playerChoiceHandler: ((Int, Int, Int) -> Void)?
/// Create and activate an activity
@objc public func begin() async {
MyActivity(ownerID: myActivityUUID).activate()
}
lazy var observer = GroupStateObserver()
/// Prepare the callback listeners and session joining logic
@objc public func setup() {
observer.$isEligibleForGroupSession.sink { isEligible in
print(isEligible ? "IS ELIGIBLE" : "NOT ELIGIBLE")
}.store(in: &subscriptions)
sessionTask?.cancel()
reset()
print("setting up!")
let task = Task {
for await session in MyActivity.sessions() {
if session.activity.ownerID == myActivityUUID {
self.hostParticipantID = session.localParticipant.id
self.leaderParticipantID = session.localParticipant.id
} else {
self.hostParticipantID = nil
}
print("GOT SESSION")
print("local participant ID - \(session.localParticipant.id)")
join(session: session)
print("joined session!")
}
}
sessionTask = task
}
@objc public func localPlayerIndex() -> Int {
guard let localParticipant = localParticipant else {
return -1
}
return registeredParticipants.firstIndex(of: localParticipant.id) ?? -1
}
@objc public func playerCount() -> Int {
return registeredParticipants.count
}
@objc public func nameForPlayer(index: Int) -> String {
let id = registeredParticipants[index]
return playerNames[id] ?? "Unknown Player Name"
}
// MARK: - Public Message Sending
@objc public func startGame() {
sendToAll(GameStartMessage())
}
@objc public func selectSounds(index1: Int, index2: Int) {
sendToAll(SelectedSoundMessage(sound1Index: index1, sound2Index: index2))
}
@objc public func selectPrompt(index: Int) {
sendToAll(SelectedPromptMessage(promptIndex: index))
}
@objc public func showResolve() {
sendToAll(ShowResolveStepMessage())
}
@objc public func pickWinner(index: Int) {
sendToAll(PickWinnerMessage(winnerIndex: index))
}
@objc public func newRound() {
guard self.isHost else { return }
let leader = self.leaderParticipantID!
let leaderIndex = self.registeredParticipants.firstIndex(of: leader)!
let newLeader = self.registeredParticipants[(leaderIndex + 1) % self.registeredParticipants.count]
self.leaderParticipantID = newLeader
self.sendToAll(NewLeaderMessage(newLeaderID: newLeader))
}
// MARK: - Private Functions
private func reset() {
playerNames = [:]
registeredParticipants = []
messenger = nil
tasks.forEach { $0.cancel() }
tasks = []
subscriptions = []
if currentSession != nil {
currentSession?.leave()
currentSession = nil
}
}
// MARK: - Private Message Sending
private func sendToAll<T: Encodable & Decodable>(_ message: T) {
messenger?.send(message) { _ in }
}
// MARK: - Event Listeners
private func join(session: GroupSession<MyActivity>) {
reset()
let messenger = GroupSessionMessenger(session: session)
self.messenger = messenger
subscribe(to: SetupMessage.self) { message in
self.hostParticipantID = message.0.hostParticipantID
self.leaderParticipantID = self.hostParticipantID
}
subscribe(to: GameStartMessage.self) { message in
// trigger game start
self.startGameHandler?()
}
subscribe(to: SelectedPromptMessage.self) { message in
// save prompt
self.selectedPromptHandler?(message.0.promptIndex)
}
subscribe(to: SelectedSoundMessage.self) { message in
// save sound selection for user
let id = message.1.source.id
self.playerChoices[id] = (message.0.sound1Index, message.0.sound2Index)
let index = self.registeredParticipants.firstIndex(of: id)!
self.playerChoiceHandler?(index, message.0.sound1Index, message.0.sound2Index)
}
subscribe(to: ShowResolveStepMessage.self) { message in
// trigger resolve step
self.showResolveHandler?()
}
subscribe(to: PickWinnerMessage.self) { message in
// update score and trigger winner screen
self.pickWinnerHandler?(message.0.winnerIndex)
}
subscribe(to: NewRoundMessage.self) { message in
// do nothing
}
subscribe(to: PlayerNamesMessage.self) { message in
self.playerNames = message.0.names
}
subscribe(to: RegisteredPlayersMessage.self) { message in
self.registeredParticipants = message.0.playerIDs
}
subscribe(to: NewLeaderMessage.self) { message in
self.leaderParticipantID = message.0.newLeaderID
}
session.$activeParticipants.sink { participants in
print("ACTIVE PARTICIPANTS UPDATED")
for person in participants {
print("\(person.id)")
}
guard self.isHost else { return }
let newParticipants = participants.subtracting(session.activeParticipants)
if !newParticipants.isEmpty, let hostID = self.hostParticipantID {
messenger.send(SetupMessage(hostParticipantID: hostID), to: .only(newParticipants)) { error in /* todo: log error */ }
for newbie in newParticipants {
let newbieID = newbie.id
let name = self.names[self.registeredParticipants.count]
self.playerNames[newbieID] = name
self.registeredParticipants.append(newbieID)
}
}
print("sending player names")
self.sendToAll(PlayerNamesMessage(names: self.playerNames))
self.sendToAll(RegisteredPlayersMessage(playerIDs: self.registeredParticipants))
}.store(in: &subscriptions)
session.$state.sink { state in
switch state {
case .waiting:
print("STATE = WAITING")
case .joined:
print("STATE = JOINED")
case .invalidated:
print("STATE = INVALIDATED")
self.reset()
@unknown default:
break
}
}.store(in: &subscriptions)
currentSession = session
session.join()
}
private func subscribe<T: Encodable & Decodable>(to messageType: T.Type, action: @escaping ((T, GroupSessionMessenger.MessageContext)) -> Void) {
let task = Task {
for await message in messenger!.messages(of: messageType) {
DispatchQueue.main.async {
action(message)
}
}
}
tasks.append(task)
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using AOT;
using TMPro;
using UnityEngine;
public class PluginHelper : MonoBehaviour
{
#region Platform-Specific DLL Importing
#if UNITY_IOS
private const string dll = "__Internal";
#else
private const string dll = "NativeUnityPlugin";
#endif
#endregion
#region Singleton Setup
public static PluginHelper Singleton;
public struct ChoiceTuple
{
public int first;
public int second;
}
private Dictionary<int, ChoiceTuple> choiceTable = new Dictionary<int, ChoiceTuple>();
private void Awake()
{
if (Singleton != null)
{
Debug.LogError("TRIED TO REPLACE SINGLETON");
Destroy(this);
return;
}
Singleton = this;
#if !UNITY_EDITOR
RegisterStartGameHandler(StartGameMessageReceived);
RegisterSelectedPromptHandler(SelectedPromptMessageReceived);
RegisterShowResolveHandler(ShowResolveMessageReceived);
RegisterPickWinnerHandler(PickWinnerMessageReceived);
RegisterPlayerChoiceHandler(PlayerChoiceMessageReceived);
#endif
}
#endregion
#region Unity Callbacks
private void Update()
{
#if UNITY_EDITOR
return;
#endif
if (Time.frameCount % 30 == 0)
{
string displayString = "";
for (int i = 0; i < PlayerCount(); i++)
{
displayString += PlayerNameAtIndex(i);
displayString += "\n";
}
debugText.text = displayString;
}
}
#endregion
public TMP_Text debugText;
#region DLL Callbacks
private delegate void StartGameDelegate();
private delegate void SelectedPromptDelegate(int index);
private delegate void ShowResolveDelegate();
private delegate void PickWinnerDelegate(int index);
private delegate void PlayerChoiceDelegate(int player, int one, int two);
[DllImport(dll)]
private static extern void RegisterPlayerChoiceHandler(PlayerChoiceDelegate callback);
[DllImport(dll)]
private static extern void RegisterSelectedPromptHandler(SelectedPromptDelegate callback);
[DllImport(dll)]
private static extern void RegisterStartGameHandler(StartGameDelegate callback);
[DllImport(dll)]
private static extern void RegisterShowResolveHandler(ShowResolveDelegate callback);
[DllImport(dll)]
private static extern void RegisterPickWinnerHandler(PickWinnerDelegate callback);
[MonoPInvokeCallback(typeof(PlayerChoiceDelegate))]
private static void PlayerChoiceMessageReceived(int player, int one, int two)
{
Singleton.choiceTable[player] = new ChoiceTuple
{
first = one,
second = two
};
// all players have picked
if (Singleton.choiceTable.Count == PlayerCount() - 1)
{
GameManager.Singleton.TransitionToNextState();
}
}
[MonoPInvokeCallback(typeof(SelectedPromptDelegate))]
private static void SelectedPromptMessageReceived(int index)
{
Debug.Log($"Got selected prompt message: {index}");
Singleton.DoSelectPrompt(index);
}
[MonoPInvokeCallback(typeof(StartGameDelegate))]
private static void StartGameMessageReceived()
{
Debug.Log("Got Start Game Message");
Singleton.DoGameStart();
}
[MonoPInvokeCallback(typeof(ShowResolveDelegate))]
private static void ShowResolveMessageReceived()
{
GameManager.Singleton.TransitionToNextState();
Debug.Log($"Got show resolve message");
}
[MonoPInvokeCallback(typeof(PickWinnerDelegate))]
private static void PickWinnerMessageReceived(int index)
{
Debug.Log($"Got Pick Winner Message {index}");
GameManager.Singleton.WinnerWasPicked(index);
}
#endregion
#region DLL Function References
[DllImport(dll)]
private static extern void SetupSharePlay();
[DllImport(dll)]
private static extern void BeginSharePlay();
[DllImport(dll)]
private static extern bool IsLeader();
[DllImport(dll)]
private static extern bool IsHost();
[DllImport(dll)]
private static extern void StartGame();
[DllImport(dll)]
private static extern void SelectSounds(int index1, int index2);
[DllImport(dll)]
private static extern void SelectPrompt(int index);
[DllImport(dll)]
private static extern void ShowResolve();
[DllImport(dll)]
private static extern void PickWinner(int index);
[DllImport(dll)]
private static extern void NewRound();
[DllImport(dll)]
private static extern int GetLocalPlayerIndex();
[DllImport(dll)]
private static extern int PlayerCount();
[DllImport(dll)]
private static extern string PlayerNameAtIndex(int index);
#endregion
#region Public Interface
public void SetupSession()
{
SetupSharePlay();
}
public void BeginSession()
{
BeginSharePlay();
}
public void SendStartGame()
{
StartGame();
}
public void DebugSendSelectedPrompt()
{
SelectPrompt(69);
}
public void SendSelectPrompt(int index)
{
promptIndex = index;
SelectPrompt(index);
}
public void SendShowResolve()
{
ShowResolve();
}
public void SendPickWinner()
{
PickWinner(420);
}
public void VoteForWinner(int index)
{
PickWinner(index);
}
public bool IsTheLeader()
{
return IsLeader();
}
public int GetPlayerCount()
{
#if !UNITY_EDITOR
return PlayerCount();
#else
return 1;
#endif
}
public ChoiceTuple GetPlayerChoice(int index)
{
return choiceTable[index];
}
public void SendNewRound()
{
NewRound();
}
public int LocalPlayerIndex()
{
return GetLocalPlayerIndex();
}
public void SetPlayerChoice(int first, int second)
{
#if !UNITY_EDITOR
var index = GetLocalPlayerIndex();
#else
var index = 0;
#endif
choiceTable[index] = new ChoiceTuple
{
first = first,
second = second
};
// all players have picked
if (choiceTable.Count == PlayerCount() - 1)
{
GameManager.Singleton.TransitionToNextState();
}
}
public string GetPlayerName(int index)
{
#if UNITY_EDITOR
return "Editor Test Name";
#endif
return PlayerNameAtIndex(index);
}
#endregion
#region Instance Handlers
private void DoGameStart()
{
Debug.Log("DoGameStart");
GameManager.Singleton.TransitionToNextState();
}
public int promptIndex = 0;
private void DoSelectPrompt(int index)
{
Debug.Log($"DoSelectPrompt {index}");
promptIndex = index;
GameManager.Singleton.TransitionToNextState();
}
#endregion
public void SendSoundChoices(int firstIndex, int secondIndex)
{
SelectSounds(firstIndex, secondIndex);
}
public bool HasPlayerChoice(int i)
{
return choiceTable.ContainsKey(i);
}
public void ResetGameState()
{
this.promptIndex = 0;
this.choiceTable.Clear();
}
}
//
// SharePlayPlugin.h
// SharePlayPlugin
//
// Created by Drew Fitzpatrick on 7/29/21.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SharePlayPlugin : NSObject
@end
NS_ASSUME_NONNULL_END
typedef void (*StartGameCallback)(void);
typedef void (*SelectedPromptCallback)(int);
typedef void (*ShowResolveCallback)(void);
typedef void (*PickWinnerCallback)(int);
typedef void (*PlayerChoiceCallback)(int, int, int);
void RegisterStartGameHandler(StartGameCallback);
void RegisterSelectedPromptHandler(SelectedPromptCallback);
void RegisterShowResolveHandler(ShowResolveCallback);
void RegisterPickWinnerHandler(PickWinnerCallback);
void RegisterPlayerChoiceHandler(PlayerChoiceCallback);
int GetLocalPlayerIndex(void);
int PlayerCount(void);
const char* PlayerNameAtIndex(int);
void SetupSharePlay(void);
void BeginSharePlay(void);
BOOL IsLeader(void);
BOOL IsHost(void);
void StartGame(void);
void SelectSounds(int index1, int index2);
//void SelectSound(char* name);
void SelectPrompt(int);
void ShowResolve(void);
void PickWinner(int);
void NewRound(void);
//
// SharePlayPlugin.m
// SharePlayPlugin
//
// Created by Drew Fitzpatrick on 7/29/21.
//
#import "SharePlayPlugin.h"
#include "UnityFramework/UnityFramework-Swift.h"
#define DllExport __attribute__(( visibility("default") ))
@implementation SharePlayPlugin
@end
DllExport void RegisterPlayerChoiceHandler(PlayerChoiceCallback callback) {
[[MyActivityController shared] setPlayerChoiceHandler:^(NSInteger index, NSInteger first, NSInteger second) {
callback((int) index, (int) first, (int) second);
}];
}
DllExport void RegisterStartGameHandler(StartGameCallback callback) {
[[MyActivityController shared] setStartGameHandler:^{
callback();
}];
}
DllExport void RegisterSelectedPromptHandler(SelectedPromptCallback callback) {
[[MyActivityController shared] setSelectedPromptHandler:^(NSInteger index) {
callback((int)index);
}];
}
DllExport void RegisterShowResolveHandler(ShowResolveCallback callback) {
[[MyActivityController shared] setShowResolveHandler:^{
callback();
}];
}
DllExport void RegisterPickWinnerHandler(PickWinnerCallback callback) {
[[MyActivityController shared] setPickWinnerHandler:^(NSInteger index) {
callback((int)index);
}];
}
DllExport int GetLocalPlayerIndex() {
return (int)[[MyActivityController shared] localPlayerIndex];
}
DllExport int PlayerCount() {
return (int)[[MyActivityController shared] playerCount];
}
DllExport const char* PlayerNameAtIndex(int index) {
NSString* name = [[MyActivityController shared] nameForPlayerWithIndex:index];
const char* cName = [name cStringUsingEncoding:NSUTF8StringEncoding];
return strdup(cName);
}
DllExport void SetupSharePlay() {
[[MyActivityController shared] setup];
}
DllExport void BeginSharePlay() {
[[MyActivityController shared] beginWithCompletionHandler:^{
// done
}];
}
DllExport BOOL IsLeader() {
return [[MyActivityController shared] isLeader];
}
DllExport BOOL IsHost() {
return [[MyActivityController shared] isHost];
}
DllExport void StartGame() {
[[MyActivityController shared] startGame];
}
DllExport void SelectSounds(int index1, int index2) {
[[MyActivityController shared] selectSoundsWithIndex1:index1 index2:index2];
}
DllExport void SelectSound(char* name) {
[[NSException exceptionWithName:@"fuck" reason:@"fuck" userInfo:nil] raise];
}
DllExport void SelectPrompt(int index) {
[[MyActivityController shared] selectPromptWithIndex:index];
}
DllExport void ShowResolve() {
[[MyActivityController shared] showResolve];
}
DllExport void PickWinner(int index) {
[[MyActivityController shared] pickWinnerWithIndex:index];
}
DllExport void NewRound() {
[[MyActivityController shared] newRound];
}
#undef DllExport
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
using System.Diagnostics;
using System.IO;
using System.Linq;
public static class SwiftPostProcess
{
[PostProcessBuild]
public static void OnPostProcessBuild(BuildTarget buildTarget, string buildPath)
{
if (buildTarget != BuildTarget.iOS) return;
var projPath = PBXProject.GetPBXProjectPath(buildPath);
var proj = new PBXProject();
proj.ReadFromFile(projPath);
var targetGuid = proj.GetUnityMainTargetGuid();
var fwTarget = proj.GetUnityFrameworkTargetGuid();
proj.SetBuildProperty(targetGuid, "IPHONEOS_DEPLOYMENT_TARGET", value: "15.0");
proj.SetBuildProperty(fwTarget, "IPHONEOS_DEPLOYMENT_TARGET", value: "15.0");
proj.WriteToFile(projPath);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment