Spatial Alignment between Two Hololens 2 Headsets using ARFoundation/Azure Spatial Anchors - unity3d

I'm working through this tutorial: https://mtaulty.com/2019/07/18/simple-shared-holograms-with-photon-networking-part-1/ with the hope of reproducing the shared coordinate system between two Hololens 2 headsets. I'm using Unity 2020, PUN2, ARFoundation and MRTK.
Because the tutorial is using WorldAnchors (WSA platform), which is a bit old, I'm trying to modify it to use ARFoundation. So far, the code I have as a result, seems to properly have the two headsets communicating via PUN2, but the blue cube as shown in the tutorial does not align between the headsets. The cube simply seems referenced to each headsets initial startup frame of reference. Below is the code. I've kept everything as one-to-one with the tutorial as possible, except where I felt I needed to swap WorldAnchors for ARAnchors and also where I swapped in a SpatialAnchorManager class to handle the Azure Spatial Service session since I found the tutorial's StartSession function didn't seem to work properly. Both AzureSpatialAnchorService.cs and PhotonScript.cs are attached to a root game object in the scene. Picture of the scene attached. Based on the debug logs I'm able to tell that the first headset is creating and saving an anchor to Azure and the second headset is able to find that same anchor. But I apparently am not performing a necessary transformation between headsets?
Can anyone suggest what I'm doing wrong and/or what specific edits that need to be made to get spatial alignment between headsets?
Thanks!
AzureSpatialAnchorService.cs:
using Microsoft.Azure.SpatialAnchors.Unity;
using Microsoft.MixedReality.Toolkit.Utilities;
using System;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.WSA;
namespace AzureSpatialAnchors
{
[RequireComponent(typeof(SpatialAnchorManager))]
public class AzureSpatialAnchorService : MonoBehaviour
{
[Serializable]
public class AzureSpatialAnchorServiceProfile
{
[SerializeField]
[Tooltip("The account id from the Azure portal for the Azure Spatial Anchors service")]
string azureAccountId;
public string AzureAccountId => this.azureAccountId;
[SerializeField]
[Tooltip("The access key from the Azure portal for the Azure Spatial Anchors service (for Key authentication)")]
string azureServiceKey;
public string AzureServiceKey => this.azureServiceKey;
}
[SerializeField]
[Tooltip("The configuration for the Azure Spatial Anchors Service")]
AzureSpatialAnchorServiceProfile profile = new AzureSpatialAnchorServiceProfile();
public AzureSpatialAnchorServiceProfile Profile => this.profile;
TaskCompletionSource<CloudSpatialAnchor> taskWaitForAnchorLocation;
//CloudSpatialAnchorSession cloudSpatialAnchorSession;
private SpatialAnchorManager _spatialAnchorManager = null;
public AzureSpatialAnchorService()
{
}
public async Task<string> CreateAnchorOnObjectAsync(GameObject gameObjectForAnchor)
{
string anchorId = string.Empty;
try
{
await this.StartSession();
Debug.Log("Started Session");
//Add and configure ASA components
CloudNativeAnchor cloudNativeAnchor = gameObjectForAnchor.AddComponent<CloudNativeAnchor>();
await cloudNativeAnchor.NativeToCloud();
Debug.Log("After NativeToCloud");
CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);
// As per previous comment.
//Collect Environment Data
while (!_spatialAnchorManager.IsReadyForCreate)
{
float createProgress = _spatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
Debug.Log($"ASA - Move your device to capture more environment data: {createProgress:0%}");
}
Debug.Log($"ASA - Saving room cloud anchor... ");
await _spatialAnchorManager.CreateAnchorAsync(cloudSpatialAnchor);
anchorId = cloudSpatialAnchor?.Identifier;
bool saveSucceeded = cloudSpatialAnchor != null;
if (!saveSucceeded)
{
Debug.LogError("ASA - Failed to save, but no exception was thrown.");
return anchorId;
}
anchorId = cloudSpatialAnchor.Identifier;
Debug.Log($"ASA - Saved room cloud anchor with ID: {anchorId}");
}
catch (Exception exception) // TODO: reasonable exceptions here.
{
Debug.Log("ASA - Failed to save room anchor: " + exception.ToString());
Debug.LogException(exception);
}
return (anchorId);
}
public async Task<bool> PopulateAnchorOnObjectAsync(string anchorId, GameObject gameObjectForAnchor)
{
bool anchorLocated = false;
try
{
await this.StartSession();
this.taskWaitForAnchorLocation = new TaskCompletionSource<CloudSpatialAnchor>();
var watcher = _spatialAnchorManager.Session.CreateWatcher(
new AnchorLocateCriteria()
{
Identifiers = new string[] { anchorId },
BypassCache = true,
Strategy = LocateStrategy.AnyStrategy,
RequestedCategories = AnchorDataCategory.Spatial
}
);
var cloudAnchor = await this.taskWaitForAnchorLocation.Task;
anchorLocated = cloudAnchor != null;
if (anchorLocated)
{
Debug.Log("Anchor located");
gameObjectForAnchor.AddComponent<CloudNativeAnchor>().CloudToNative(cloudAnchor);
Debug.Log("Attached Local Anchor");
}
watcher.Stop();
}
catch (Exception ex) // TODO: reasonable exceptions here.
{
Debug.Log($"Caught {ex.Message}");
}
return (anchorLocated);
}
/// <summary>
/// Start the Azure Spatial Anchor Service session
/// This must be called before calling create, populate or delete methods.
/// </summary>
public async Task<bool> StartSession()
{
//if (this.cloudSpatialAnchorSession == null)
//{
// Debug.Assert(this.cloudSpatialAnchorSession == null);
// this.ThrowOnBadAuthConfiguration();
// // setup the session
// this.cloudSpatialAnchorSession = new CloudSpatialAnchorSession();
// // set the Azure configuration parameters
// this.cloudSpatialAnchorSession.Configuration.AccountId = this.Profile.AzureAccountId;
// this.cloudSpatialAnchorSession.Configuration.AccountKey = this.Profile.AzureServiceKey;
// // register event handlers
// this.cloudSpatialAnchorSession.Error += this.OnCloudSessionError;
// this.cloudSpatialAnchorSession.AnchorLocated += OnAnchorLocated;
// this.cloudSpatialAnchorSession.LocateAnchorsCompleted += OnLocateAnchorsCompleted;
// // start the session
// this.cloudSpatialAnchorSession.Start();
//}
_spatialAnchorManager = GetComponent<SpatialAnchorManager>();
_spatialAnchorManager.LogDebug += (sender, args) => Debug.Log($"ASA - Debug: {args.Message}");
_spatialAnchorManager.Error += (sender, args) => Debug.LogError($"ASA - Error: {args.ErrorMessage}");
_spatialAnchorManager.AnchorLocated += OnAnchorLocated;
//_spatialAnchorManager.LocateAnchorsCompleted += OnLocateAnchorsCompleted;
await _spatialAnchorManager.StartSessionAsync();
return true;
}
/// <summary>
/// Stop the Azure Spatial Anchor Service session
/// </summary>
//public void StopSession()
//{
// if (this.cloudSpatialAnchorSession != null)
// {
// // stop session
// this.cloudSpatialAnchorSession.Stop();
// // clear event handlers
// this.cloudSpatialAnchorSession.Error -= this.OnCloudSessionError;
// this.cloudSpatialAnchorSession.AnchorLocated -= OnAnchorLocated;
// this.cloudSpatialAnchorSession.LocateAnchorsCompleted -= OnLocateAnchorsCompleted;
// // cleanup
// this.cloudSpatialAnchorSession.Dispose();
// this.cloudSpatialAnchorSession = null;
// }
//}
void OnLocateAnchorsCompleted(object sender, LocateAnchorsCompletedEventArgs args)
{
Debug.Log("On Locate Anchors Completed");
Debug.Assert(this.taskWaitForAnchorLocation != null);
if (!this.taskWaitForAnchorLocation.Task.IsCompleted)
{
this.taskWaitForAnchorLocation.TrySetResult(null);
}
}
void OnAnchorLocated(object sender, AnchorLocatedEventArgs args)
{
Debug.Log($"On Anchor Located, status is {args.Status} anchor is {args.Anchor?.Identifier}, pointer is {args.Anchor?.LocalAnchor}");
Debug.Assert(this.taskWaitForAnchorLocation != null);
this.taskWaitForAnchorLocation.SetResult(args.Anchor);
}
void OnCloudSessionError(object sender, SessionErrorEventArgs args)
{
Debug.Log($"On Cloud Session Error: {args.ErrorMessage}");
}
void ThrowOnBadAuthConfiguration()
{
if (string.IsNullOrEmpty(this.Profile.AzureAccountId) ||
string.IsNullOrEmpty(this.Profile.AzureServiceKey))
{
throw new ArgumentNullException("Missing required configuration to connect to service");
}
}
}
}
PhotonScript.cs:
using System;
using System.Threading.Tasks;
using AzureSpatialAnchors;
using ExitGames.Client.Photon;
using Photon.Pun;
using Photon.Realtime;
public class PhotonScript : MonoBehaviourPunCallbacks
{
enum RoomStatus
{
None,
CreatedRoom,
JoinedRoom,
JoinedRoomDownloadedAnchor
}
public int emptyRoomTimeToLiveSeconds = 120;
RoomStatus roomStatus = RoomStatus.None;
void Start()
{
PhotonNetwork.ConnectUsingSettings();
}
public override void OnConnectedToMaster()
{
base.OnConnectedToMaster();
var roomOptions = new RoomOptions();
roomOptions.EmptyRoomTtl = this.emptyRoomTimeToLiveSeconds * 1000;
PhotonNetwork.JoinOrCreateRoom(ROOM_NAME, roomOptions, null);
}
public async override void OnJoinedRoom()
{
base.OnJoinedRoom();
// Note that the creator of the room also joins the room...
if (this.roomStatus == RoomStatus.None)
{
this.roomStatus = RoomStatus.JoinedRoom;
}
await this.PopulateAnchorAsync();
}
public async override void OnCreatedRoom()
{
base.OnCreatedRoom();
this.roomStatus = RoomStatus.CreatedRoom;
await this.CreateAnchorAsync();
}
async Task CreateAnchorAsync()
{
// If we created the room then we will attempt to create an anchor for the parent
// of the cubes that we are creating.
var anchorService = this.GetComponent<AzureSpatialAnchorService>();
var anchorId = await anchorService.CreateAnchorOnObjectAsync(this.gameObject);
// Put this ID into a custom property so that other devices joining the
// room can get hold of it.
#if UNITY_2020
PhotonNetwork.CurrentRoom.SetCustomProperties(
new Hashtable()
{
{ ANCHOR_ID_CUSTOM_PROPERTY, anchorId }
}
);
#endif
}
async Task PopulateAnchorAsync()
{
if (this.roomStatus == RoomStatus.JoinedRoom)
{
object keyValue = null;
#if UNITY_2020
// First time around, this property may not be here so we see if is there.
if (PhotonNetwork.CurrentRoom.CustomProperties.TryGetValue(
ANCHOR_ID_CUSTOM_PROPERTY, out keyValue))
{
// If the anchorId property is present then we will try and get the
// anchor but only once so change the status.
this.roomStatus = RoomStatus.JoinedRoomDownloadedAnchor;
// If we didn't create the room then we want to try and get the anchor
// from the cloud and apply it.
var anchorService = this.GetComponent<AzureSpatialAnchorService>();
await anchorService.PopulateAnchorOnObjectAsync(
(string)keyValue, this.gameObject);
}
#endif
}
}
public async override void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)
{
base.OnRoomPropertiesUpdate(propertiesThatChanged);
await this.PopulateAnchorAsync();
}
static readonly string ANCHOR_ID_CUSTOM_PROPERTY = "anchorId";
static readonly string ROOM_NAME = "HardCodedRoomName";
}

From reviewing the code and scenario, I am reading that this is in same local area. So, the ASA service will have the shared anchor as detailed in docs:
https://learn.microsoft.com/en-us/windows/mixed-reality/design/shared-experiences-in-mixed-reality
"Shared static holograms (no interactions)
Leverage Azure Spatial Anchors in your app. Enabling and sharing spatial anchors across devices allows you to create an application where users see holograms in the same place at the same time. Additional syncing across devices is needed to enable users to interact with holograms and see movements or state updates of holograms."
The tutorial that Microsoft docs point to is here if it helps comparison to the other sample:
https://learn.microsoft.com/en-us/windows/mixed-reality/develop/unity/tutorials/mr-learning-sharing-01

Related

Get list of audio mp3 files and play them in Unity 3d

I'm trying to build an audio player that gets the mp3 files from an external source along with logic for Play, Stop, Next and Prev buttons but I haven't figured out how to do it.
http://localhost:8383/Unity3d/Audio/BT - Paul Van Dyk - Namistai.mp3"
http://localhost:8383/Unity3d/Audio/Stevie Wonder - Skeletons.mp3
And then with C#, I'm opening a UnityWebRequest for the URI, save it into an AudioClip variable and then to an AudioSource variable but I'm then finding issues with using the length property/saving into some sort of Audio clip array.
Here's my code below:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.Networking;
using UnityEngine.UI;
[RequireComponent(typeof(AudioSource))]
public class AudioManager : MonoBehaviour
{
public AudioClip[] musicClips;
private int currentTrack;
private AudioSource source;
public List<string> externalAudio;
IEnumerator GetAudioClip()
{
externalAudio.Add("http://localhost:8383/Unity3d/Audio/Stevie%20Wonder%20-%20Skeletons.mp3");
externalAudio.Add("http://localhost:8383/Unity3d/Audio/York%20-%20On%20The%20Beach.mp3");
externalAudio.Add("http://localhost:8383/Unity3d/Audio/BT%20-%20Paul%20Van%20Dyk%20-%20Namistai.mp3");
externalAudio.Add("http://localhost:8383/Unity3d/Audio/S.O.S.%20BAND-JUST%20BE%20GOOD%20TO%20ME%20(SINGLE).mp3");
externalAudio.Add("http://localhost:8383/Unity3d/Audio/Michael%20Jackson%20-%20Beat%20It%20(Official%20Video).mp3");
Debug.Log("Hello World");
Debug.Log(UnityWebRequestMultimedia.GetAudioClip("http://localhost:8383/Unity3d/Audio/Stevie%20Wonder%20-%20Skeletons.mp3", AudioType.MPEG));
foreach (var audioClip in externalAudio)
{
Debug.Log(audioClip);
using (UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(audioClip, AudioType.MPEG))
{
UnityWebRequest request = new UnityWebRequest();
yield return www.SendWebRequest();
if (request.result == UnityWebRequest.Result.ConnectionError)
{
Debug.Log(www.error);
}
else
{
musicClips = DownloadHandlerAudioClip.GetContent(www);
source.clip = musicClips;
Debug.Log("Audio is playing.");
}
}
}
}
// Start is called before the first frame update
void Start()
{
source = GetComponent<AudioSource>();
//PLAY MUSIC
PlayMusic();
}
public void PlayMusic()
{
if (source.isPlaying)
{
return;
}
GetAudioClip();
currentTrack--;
if (currentTrack < 0)
{
currentTrack = 0;
}
StartCoroutine(WaitForMusicEnd());
}
IEnumerator WaitForMusicEnd()
{
while (source.isPlaying)
{
yield return null;
}
NextTitle();
}
public void NextTitle()
{
source.Stop();
currentTrack++;
if (currentTrack > musicClips.Length - 1)
{
currentTrack = 0;
}
source.clip = musicClips[currentTrack];
source.Play();
//Show title
StartCoroutine("WaitForMusicEnd");
}
public void PreviousTitle()
{
source.Stop();
currentTrack--;
if (currentTrack < 0)
{
currentTrack = musicClips.Length - 1;
}
source.clip = musicClips[currentTrack];
source.Play();
//Show title
StartCoroutine("WaitForMusicEnd");
}
public void StopMusic()
{
StopCoroutine("WaitForMusicEnd");
source.Stop();
}
}
How can I save the external audio into an AudioClip list and then use them on the play/stop functions logic?
So this is a little bit broad but here is what I would probably do.
As said let your server provide a list of URLs for the downloads. This way you don't have to hardcode them into your client.
So with the previous point, the first thing you do is one single normal GET request to receive that list of URLs, e.g. simply separated by \n (line breaks)
So the first string message you receive could look like e.g.
http://localhost:8383/Moonbeam%20Challenge/Audio/Stevie%20Wonder%20-%20Skeletons.mp3
http://localhost:8383/Moonbeam%20Challenge/Audio/York%20-%20On%20The%20Beach.mp3
http://localhost:8383/Moonbeam%20Challenge/Audio/BT%20-%20Paul%20Van%20Dyk%20-%20Namistai.mp3
http://localhost:8383/Moonbeam%20Challenge/Audio/S.O.S.%20BAND-JUST%20BE%20GOOD%20TO%20ME%20(SINGLE).mp3
http://localhost:8383/Moonbeam%20Challenge/Audio/Michael%20Jackson%20-%20Beat%20It%20(Official%20Video).mp3
This received list you Split into the individual URLs
var urls = urlList.Split('\n');
For these you start your individual GetAudioClip requests.
The results you store in an array/a list as elements. You don't assign the entire musicClips. I would rather use a
List<AudioClip> musicClips = new List<AudioClip>();
and then from your downloads do
musicClips.Add(DownloadHandlerAudioClip.GetContent(www));
you also don't want to assign and play each downloaded clip to the AudioSource as soon as it is downloaded but rather wait until all of them are downloaded (or at least the first one, depending on your needs)
So this might look somewhat like
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.Networking;
using UnityEngine.UI;
using System.Linq;
[RequireComponent(typeof(AudioSource))]
public class AudioManager : MonoBehaviour
{
[Header("References")]
[SerializeField]
private AudioSource source;
[Header("Settings")]
[SerializeField] private string baseURL;
[SerializeField] private string[] externalAudio;
[SerializeField] private bool autoStartPlayAfterDownloads = true;
[Header("Debugging")]
[SerializeField] private List<AudioClip> musicClips = new List<AudioClip>();
[SerializeField] private int currentTrack;
[SerializeField] private bool isInitialized;
private Coroutine currentPlayTrack;
// Yes, if you make Start return IEnumerator then Units
// automatically runs it as a Coroutine
private IEnumerator Start()
{
// block input from the outside until this controller is finished with the downloads
isInitialized = false;
if (!source) source = GetComponent<AudioSource>();
// Here either use the approach with first receiving a list from the server
yield return GetAudioClipsInfo();
// OR if you already have the clip urls assigned via the Inspector use one of
yield return GetAudioClipsSequencial();
// OR
yield return GetAudioClipsParallel();
// allow to do things from this point on
isInitialized = true;
if(autoStartPlayAfterDownloads) PlayFirstTitle();
}
// This is the routine that downloads the URL list from the server
// then it starts the individual downloads
private IEnumerator GetAudioClipsInfo()
{
// Make the first request for receiving the list of URLs from the server
// If not using this but already assigning the urls
// via Inspector then simply directly use the "GetAudioClips" below
using (var request = UnityWebRequest.Get(baseURL))
{
yield return request.SendWebRequest();
switch (request.result)
{
case UnityWebRequest.Result.ConnectionError:
case UnityWebRequest.Result.DataProcessingError:
case UnityWebRequest.Result.ProtocolError:
Debug.LogError($"Could not get list of clips! Error: {request.error}", this);
yield break;
}
var urlList = request.downloadHandler.text;
externalAudio = urlList.Split('\n');
// Here you can either go for the sequencial downloads
yield return GetAudioClipsSequencial();
// OR run them all parallel
yield return GetAudioClipsParallel();
}
}
// This version starts one download at a time, waits until it is done
// then starts the next
private IEnumerator GetAudioClipsSequencial()
{
musicClips.Clear();
foreach (var url in externalAudio)
{
Debug.Log(url);
using (var www = UnityWebRequestMultimedia.GetAudioClip(url, AudioType.MPEG))
{
yield return www.SendWebRequest();
switch (www.result)
{
case UnityWebRequest.Result.ConnectionError:
case UnityWebRequest.Result.DataProcessingError:
case UnityWebRequest.Result.ProtocolError:
Debug.LogError($"Could not get clip from \"{url}\"! Error: {www.error}", this);
continue;
}
musicClips.Add(DownloadHandlerAudioClip.GetContent(www));
}
}
}
// This version starts all downloads at once and waits until they are all done
// probably faster than the sequencial version
private IEnumerator GetAudioClipsParallel()
{
musicClips.Clear();
var requests = new List<UnityWebRequest>();
foreach (var url in externalAudio)
{
Debug.Log(url);
var www = UnityWebRequestMultimedia.GetAudioClip(url, AudioType.MPEG);
// Start the request without waiting
www.SendWebRequest();
requests.Add(www);
}
// Wait for all requests to finish
yield return new WaitWhile(() => requests.Any(r => !r.isDone));
// Now examine and use all results
foreach (var www in requests)
{
switch (www.result)
{
case UnityWebRequest.Result.ConnectionError:
case UnityWebRequest.Result.DataProcessingError:
case UnityWebRequest.Result.ProtocolError:
Debug.LogError($"Could not get clip from \"{www.url}\"! Error: {www.error}", this);
continue;
}
musicClips.Add(DownloadHandlerAudioClip.GetContent(www));
www.Dispose();
}
}
public void PlayFirstTitle()
{
if (!isInitialized) return;
if (source.isPlaying) return;
if (currentPlayTrack != null)
{
StopCoroutine(currentPlayTrack);
}
currentPlayTrack = StartCoroutine(PlayTrack(0));
}
private IEnumerator PlayTrack(int index)
{
// Make sure the index is within the given clips range
index = Mathf.Clamp(index, 0, musicClips.Count);
// update the current track to make next and previous work
currentTrack = index;
// get clip by index
var clip = musicClips[currentTrack];
// Assign and play
source.clip = clip;
source.Play();
// wait for clip end
while (source.isPlaying)
{
yield return null;
}
NextTitle();
}
public void NextTitle()
{
if (!isInitialized) return;
if (currentPlayTrack != null)
{
StopCoroutine(currentPlayTrack);
}
source.Stop();
currentTrack = (currentTrack + 1) % musicClips.Count;
currentPlayTrack = StartCoroutine(PlayTrack(currentTrack));
}
public void PreviousTitle()
{
if (!isInitialized) return;
if (currentPlayTrack != null)
{
StopCoroutine(currentPlayTrack);
}
source.Stop();
currentTrack--;
if (currentTrack < 0)
{
currentTrack = musicClips.Count - 1;
}
currentPlayTrack = StartCoroutine(PlayTrack(currentTrack));
}
public void StopMusic()
{
if (!isInitialized) return;
if (currentPlayTrack != null)
{
StopCoroutine(currentPlayTrack);
}
source.Stop();
}
}

How to Create an Input Name Script in Unity [duplicate]

This question already has answers here:
How to pass data (and references) between scenes in Unity
(6 answers)
Closed 2 years ago.
I have a name input script in the first scene, the plan is I want to call this input in the second scene, when I enter the name in the first scene, then the name will appear in the second scene too, how do you do that?
public class NamaUser : MonoBehaviour {
public InputField nama;
public Text teks;
public void NamaTeks () {
if (nama.text == "") {
teks.text = "Harap Isi Nama";
} else {
teks.text = "Namaku " + nama.text;
}
}
}
You can save the input's value PlayerPrefs.
Set the PlayerPrefs:
//Name of Pref in first parameter
//Value in second parameter
PlayerPrefs.SetString("value", teks.value);
Get the PlayerPref in second scene:
//Name of Pref in first parameter
//Returns value of PlayerPrefs
String a = PlayerPrefs.SetString("value");
Cons:
You can pass data not only between scenes but also between instances (game sessions).
Easy to manage since Unity handles all background process.
Can be used to store data to track highscores.
Pros:
Uses file system.
Data can easily be changed from prefs file.
Or, another way -- use Singelton and DontDestroyOnLoad()
Allows easy access to fields and saves an object between scenes.
For example use this template, to create your class.
using UnityEngine;
public class Singelton<T> : MonoBehaviour where T : Singelton<T>
{
private static T instance = null;
private bool alive = true;
public static T Instance
{
get
{
if (instance != null)
{
return instance;
}
else
{
//Find T
T[] managers = GameObject.FindObjectsOfType<T>();
if (managers != null)
{
if (managers.Length == 1)
{
instance = managers[0];
DontDestroyOnLoad(instance);
return instance;
}
else
{
if (managers.Length > 1)
{
Debug.LogError($"Have more that one {typeof(T).Name} in scene. " +
"But this is Singelton! Check project.");
for (int i = 0; i < managers.Length; ++i)
{
T manager = managers[i];
Destroy(manager.gameObject);
}
}
}
}
//create
GameObject go = new GameObject(typeof(T).Name, typeof(T));
instance = go.GetComponent<T>();
DontDestroyOnLoad(instance.gameObject);
return instance;
}
}
//Can be initialized externally
set
{
instance = value as T;
}
}
/// <summary>
/// Check flag if need work from OnDestroy or OnApplicationExit
/// </summary>
public static bool IsAlive
{
get
{
if (instance == null)
return false;
return instance.alive;
}
}
protected virtual void Awake()
{
if (instance == null)
{
DontDestroyOnLoad(gameObject);
instance = this as T;
}
else
{
Debug.LogError($"Have more that one {typeof(T).Name} in scene. " +
"But this is Singelton! Check project.");
Destroy(gameObject);
}
}
protected virtual void OnDestroy() { alive = false; }
protected virtual void OnApplicationQuit() { alive = false; }
}
Example of using:
class MyClass Settings : Singelton<Settings>
{
string param;
}

How can i store or read a animation clip data in runtime?

I'm working on a small program that can modify the animation at run time(Such as when you run faster the animation not only play faster but also with larger movement). So i need to get the existing animation, change its value, then send it back.
I found it is interesting that i can set a new curve to the animation, but i can't get access to what i already have. So I either write a file to store my animation curve (as text file for example), or i find someway to read the animation on start up.
I tried to use
AnimationUtility.GetCurveBindings(AnimationCurve);
It worked in my testing, but in some page it says this is a "Editor code", that if i build the project into a standalone program it will not work anymore. Is that true? If so, is there any way to get the curve at run time?
Thanks to the clearify from Benjamin Zach and suggestion from TehMightyPotato
I'd like to keep the idea about modifying the animation at runtime. Because it could adapt to more situations imo.
My idea for now is to write a piece of editor code that can read from the curve in Editor and output all necesseary information about the curve (keyframes) into a text file. Then read that file at runtime and create new curve to overwrite the existing one. I will leave this question open for a few days and check it to see if anyone has a better idea about it.
As said already AnimationUtility belongs to the UnityEditor namespace. This entire namespace is completely stripped of in a build and nothing in it will be available in the final app but only within the Unity Editor.
Store AnimationCurves to file
In order to store all needed information to a file you could have a script for once serializing your specific animation curve(s) in the editor before building using e.g. BinaryFormatter.Serialize. Then later on runtime you can use BinaryFormatter.Deserialize for returning the info list again.
If you wanted it more editable you could as well use e.g. JSON or XML of course
UPDATE: In general Stop using BinaryFormatter!
In the newest Unity versions the Newtonsoft Json.NET package comes already preinstalled so simply rather use JSON
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Unity.Plastic.Newtonsoft.Json;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
public class AnimationCurveManager : MonoBehaviour
{
[Serializable]
public sealed class ClipInfo
{
public int ClipInstanceID;
public List<CurveInfo> CurveInfos = new List<CurveInfo>();
// default constructor is sometimes required for (de)serialization
public ClipInfo() { }
public ClipInfo(Object clip, List<CurveInfo> curveInfos)
{
ClipInstanceID = clip.GetInstanceID();
CurveInfos = curveInfos;
}
}
[Serializable]
public sealed class CurveInfo
{
public string PathKey;
public List<KeyFrameInfo> Keys = new List<KeyFrameInfo>();
public WrapMode PreWrapMode;
public WrapMode PostWrapMode;
// default constructor is sometimes required for (de)serialization
public CurveInfo() { }
public CurveInfo(string pathKey, AnimationCurve curve)
{
PathKey = pathKey;
foreach (var keyframe in curve.keys)
{
Keys.Add(new KeyFrameInfo(keyframe));
}
PreWrapMode = curve.preWrapMode;
PostWrapMode = curve.postWrapMode;
}
}
[Serializable]
public sealed class KeyFrameInfo
{
public float Value;
public float InTangent;
public float InWeight;
public float OutTangent;
public float OutWeight;
public float Time;
public WeightedMode WeightedMode;
// default constructor is sometimes required for (de)serialization
public KeyFrameInfo() { }
public KeyFrameInfo(Keyframe keyframe)
{
Value = keyframe.value;
InTangent = keyframe.inTangent;
InWeight = keyframe.inWeight;
OutTangent = keyframe.outTangent;
OutWeight = keyframe.outWeight;
Time = keyframe.time;
WeightedMode = keyframe.weightedMode;
}
}
// I know ... singleton .. but what choices do we have? ;)
private static AnimationCurveManager _instance;
public static AnimationCurveManager Instance
{
get
{
// lazy initialization/instantiation
if (_instance) return _instance;
_instance = FindObjectOfType<AnimationCurveManager>();
if (_instance) return _instance;
_instance = new GameObject("AnimationCurveManager").AddComponent<AnimationCurveManager>();
return _instance;
}
}
// Clips to manage e.g. reference these via the Inspector
public List<AnimationClip> clips = new List<AnimationClip>();
// every animation curve belongs to a specific clip and
// a specific property of a specific component on a specific object
// for making this easier lets simply use a combined string as key
private string CurveKey(string pathToObject, Type type, string propertyName)
{
return $"{pathToObject}:{type.FullName}:{propertyName}";
}
public List<ClipInfo> ClipCurves = new List<ClipInfo>();
private string filePath = Path.Combine(Application.streamingAssetsPath, "AnimationCurves.dat");
private void Awake()
{
if (_instance && _instance != this)
{
Debug.LogWarning("Multiple Instances of AnimationCurveManager! Will ignore this one!", this);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
// load infos on runtime
LoadClipCurves();
}
#if UNITY_EDITOR
// Call this from the ContextMenu (or later via editor script)
[ContextMenu("Save Animation Curves")]
private void SaveAnimationCurves()
{
ClipCurves.Clear();
foreach (var clip in clips)
{
var curveInfos = new List<CurveInfo>();
ClipCurves.Add(new ClipInfo(clip, curveInfos));
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
var key = CurveKey(binding.path, binding.type, binding.propertyName);
var curve = AnimationUtility.GetEditorCurve(clip, binding);
curveInfos.Add(new CurveInfo(key, curve));
}
}
// create the StreamingAssets folder if it does not exist
try
{
if (!Directory.Exists(Application.streamingAssetsPath))
{
Directory.CreateDirectory(Application.streamingAssetsPath);
}
}
catch (IOException ex)
{
Debug.LogError(ex.Message);
}
// create a new file e.g. AnimationCurves.dat in the StreamingAssets folder
var json = JsonConvert.SerializeObject(ClipCurves);
File.WriteAllText(filePath, json);
AssetDatabase.Refresh();
}
#endif
private void LoadClipCurves()
{
if (!File.Exists(filePath))
{
Debug.LogErrorFormat(this, "File \"{0}\" not found!", filePath);
return;
}
var fileStream = new FileStream(filePath, FileMode.Open);
var json = File.ReadAllText(filePath);
ClipCurves = JsonConvert.DeserializeObject<List<ClipInfo>>(json);
}
// now for getting a specific clip's curves
public AnimationCurve GetCurve(AnimationClip clip, string pathToObject, Type type, string propertyName)
{
// either not loaded yet or error -> try again
if (ClipCurves == null || ClipCurves.Count == 0) LoadClipCurves();
// still null? -> error
if (ClipCurves == null || ClipCurves.Count == 0)
{
Debug.LogError("Apparantly no clipCurves loaded!");
return null;
}
var clipInfo = ClipCurves.FirstOrDefault(ci => ci.ClipInstanceID == clip.GetInstanceID());
// does this clip exist in the dictionary?
if (clipInfo == null)
{
Debug.LogErrorFormat(this, "The clip \"{0}\" was not found in clipCurves!", clip.name);
return null;
}
var key = CurveKey(pathToObject, type, propertyName);
var curveInfo = clipInfo.CurveInfos.FirstOrDefault(c => string.Equals(c.PathKey, key));
// does the curve key exist for the clip?
if (curveInfo == null)
{
Debug.LogErrorFormat(this, "The key \"{0}\" was not found for clip \"{1}\"", key, clip.name);
return null;
}
var keyframes = new Keyframe[curveInfo.Keys.Count];
for (var i = 0; i < curveInfo.Keys.Count; i++)
{
var keyframe = curveInfo.Keys[i];
keyframes[i] = new Keyframe(keyframe.Time, keyframe.Value, keyframe.InTangent, keyframe.OutTangent, keyframe.InWeight, keyframe.OutWeight)
{
weightedMode = keyframe.WeightedMode
};
}
var curve = new AnimationCurve(keyframes)
{
postWrapMode = curveInfo.PostWrapMode,
preWrapMode = curveInfo.PreWrapMode
};
// otherwise finally return the AnimationCurve
return curve;
}
}
Then you can do something like e.e.
AnimationCurve originalCurve = AnimationCurvesManager.Instance.GetCurve(
clip,
"some/relative/GameObject",
typeof<SomeComponnet>,
"somePropertyName"
);
the second parameter pathToObject is an empty string if the property/component is attached to the root object itself. Otherwise it is given in the hierachy path as usual for Unity like e.g. "ChildName/FurtherChildName".
Now you can change the values and assign a new curve on runtime.
Assigning new curve on runtime
On runtime you can use animator.runtimeanimatorController in order to retrieve a RuntimeAnimatorController reference.
It has a property animationClips which returns all AnimationClips assigned to this controller.
You could then use e.g. Linq FirstOrDefault in order to find a specific AnimationClip by name and finally use AnimationClip.SetCurve to assign a new animation curve to a certain component and property.
E.g. something like
// you need those of course
string clipName;
AnimationCurve originalCurve = AnimationCurvesManager.Instance.GetCurve(
clip,
"some/relative/GameObject",
typeof<SomeComponnet>,
"somePropertyName"
);
// TODO
AnimationCurve newCurve = SomeMagic(originalCurve);
// get the animator reference
var animator = animatorObject.GetComponent<Animator>();
// get the runtime Animation controller
var controller = animator.runtimeAnimatorController;
// get all clips
var clips = controller.animationClips;
// find the specific clip by name
// alternatively you could also get this as before using a field and
// reference the according script via the Inspector
var someClip = clips.FirstOrDefault(clip => string.Equals(clipName, clip.name));
// was found?
if(!someClip)
{
Debug.LogWarningFormat(this, "There is no clip called {0}!", clipName);
return;
}
// assign a new curve
someClip.SetCurve("relative/path/to/some/GameObject", typeof(SomeComponnet), "somePropertyName", newCurve);
Note: Typed on smartphone so no warranty! But I hope the idea gets clear...
Also checkout the example in AnimationClip.SetCurve → You might want to use the Animation component instead of an Animator in your specific use case.

Photon, PlayFab, & Unity3D - Players not appearing to one another after joining Photon Room

I am working on a multiplayer game in Unity which is using Playfab and the Authentication and Photon which is hosting the multiplayer. I can successfully get players into the same room and I can load the scene after players 'join' the room, however, when 2 players are in the same room, they can not see each other.
This is my authentication service:
public class LoginWithCustomID : MonoBehaviour
{
private string _playFabPlayerIdCache;
private bool _isNewAccount;
private string _playerName;
// Use this to auth normally for PlayFab
void Awake()
{
PhotonNetwork.autoJoinLobby = false;
PhotonNetwork.automaticallySyncScene = true;
DontDestroyOnLoad(gameObject);
authenticateWithPlayfab();
}
private void authenticateWithPlayfab()
{
var request = new LoginWithCustomIDRequest
{
CustomId = "CustomId123",
CreateAccount = true,
InfoRequestParameters = new GetPlayerCombinedInfoRequestParams()
{
GetUserAccountInfo = true,
ProfileConstraints = new PlayerProfileViewConstraints()
{ ShowDisplayName = true }
}
};
PlayFabClientAPI.LoginWithCustomID(request, requestPhotonToken, OnLoginFailure);
}
private void requestPhotonToken(LoginResult result)
{
PlayerAccountService.loginResult = result;
_playFabPlayerIdCache = result.PlayFabId;
_playerName = result.InfoResultPayload.AccountInfo.TitleInfo.DisplayName;
if (result.NewlyCreated)
{
_isNewAccount = true;
setupNewPlayer(result);
}
PlayFabClientAPI.GetPhotonAuthenticationToken(new GetPhotonAuthenticationTokenRequest()
{
PhotonApplicationId = "photonId123"
}, AuthenticateWithPhoton, OnLoginFailure);
}
private void setupNewPlayer(LoginResult result)
{
PlayFabClientAPI.UpdateUserData(
new UpdateUserDataRequest()
{
Data = new Dictionary<string, string>()
{
{ "Level", "1" },
{ "xp", "0" }
}
}, success =>
{
Debug.Log("Set User Data");
}, failure =>
{
Debug.Log("Failed to set User Data..");
}
);
}
private void AuthenticateWithPhoton(GetPhotonAuthenticationTokenResult result)
{
Debug.Log("Photon token acquired: " + result.PhotonCustomAuthenticationToken);
var customAuth = new AuthenticationValues { AuthType = CustomAuthenticationType.Custom };
customAuth.AddAuthParameter("username", _playFabPlayerIdCache);
customAuth.AddAuthParameter("token", result.PhotonCustomAuthenticationToken);
PhotonNetwork.AuthValues = customAuth;
setNextScene();
}
private void setNextScene()
{
if(_isNewAccount || _playerName == null)
{
SceneManager.LoadSceneAsync("CreatePlayerName", LoadSceneMode.Single);
}
else
{
SceneManager.LoadSceneAsync("LandingScene", LoadSceneMode.Single);
}
}
private void OnLoginFailure(PlayFabError error)
{
Debug.LogWarning("something went wrong in auth login");
Debug.LogError("Here's some debug info:");
Debug.LogError(error.GenerateErrorReport());
}
}
}
This all works and a player is logged into PlayFab, as well as Photon I would assume if I got the Photon auth token. This brings me to my landing scene, which is essentially a place for an authenticated user to click a button to join a random room via Photon:
public static GameManager instance;
public static GameObject localPlayer;
private void Awake()
{
if (instance != null)
{
DestroyImmediate(instance);
return;
}
DontDestroyOnLoad(gameObject);
instance = this;
PhotonNetwork.automaticallySyncScene = true;
}
// Use this for initialization
void Start()
{
PhotonNetwork.ConnectUsingSettings("A_0.0.1");
}
public void JoinGame()
{
RoomOptions ro = new RoomOptions();
ro.MaxPlayers = 4;
PhotonNetwork.JoinOrCreateRoom("Test Room 2", ro, null);
}
public override void OnJoinedRoom()
{
Debug.Log("Joined Room!");
if (PhotonNetwork.isMasterClient)
{
PhotonNetwork.LoadLevel("Test_Map1");
}
}
private void OnLevelWasLoaded(int level)
{
if (!PhotonNetwork.inRoom)
return;
localPlayer = PhotonNetwork.Instantiate(
"Player",
new Vector3(0, 1f, 0),
Quaternion.identity,
0);
}
public void LeaveRoom()
{
PhotonNetwork.LeaveRoom();
SceneManager.LoadScene("LandingScene", LoadSceneMode.Single);
}
This loads the scene that I named "Test_scene1" successfully and I show within my scene, the room name and number of active players in the room. When I do a run and build, I get a user's playerPrefab to load into the room. When I run the game through unity, I can get a second player to log into the room. The problem is, the players do not see eachother and I can not figure out why that is. I am following the PLayerfab/Photon tutorials on their respective sites, but I can't find anything that I did wrong in either one.
From what I read, it looks like my instantiate method might be wrong but I'm not sure why. Below is my player Prefab showing the components attached to it:
I apologize for this huge question, I just wanted to provide as much information as I could.
This question was answered by the OP on PlayFab forums here.

Windows phone 8 and Unity 5 support for Facebook SDK

This is a request for support for WP8 and Unity 5 for the Facebook SDK.
I am a developer wanting to produce games across many platforms, and being able to publish to the Windows Phone 8 store is a big part of my agenda.
Is support for this platform scheduled to be released any time soon? If so, when are you looking to release it?
Also, is there an imminent release for the Unity SDK that is designed to work with Unity 5? I have managed to get the current release to work, but as it is not yet fully supported, I do not know what will work and what won't.
Any information regarding these issues would be much appreciated!
Update: I ran into this video that explains how the "AndContinue" methods are going away in windows 10 so we can use one single method; the Async methods that already exists on windows 8/8.1 api. Check it out at https://youtu.be/aFVAP3fNJVo?t=23m34s
I was in the same place as you but managed to get it working. When I have time, ill probably write a blog on my experiences.
Here is some of the main code that directly calls on WebAuthenticationBroker:-
static bool isTryingToRegister { get; set; }
private static string _FbToken;
public static string response;
static bool isLoggedIn { get; set; }
static private string AppID { get { return "000000000000000000"; } }
static private Uri callback { get; set; }
static private string permissions { get { return "public_profile, user_friends, email, publish_actions, user_photos"; } }
public static System.Action<object> AuthSuccessCallback;
public static System.Action<object> AuthFailedCallback;
public static string fbToken
{
get
{
return _FbToken;
}
}
static Uri loginUrl
{
get
{
return new Uri(String.Format("https://www.facebook.com/v2.0/dialog/oauth/?client_id={0}&display=popup&response_type=token&redirect_uri={1}&scope={2}",
AppID,
callback,
permissions));
}
}
public static IEnumerator _Run_ConnectWithFacebook()
{
yield return new WaitForEndOfFrame();
#if UNITY_EDITOR
UnityDebugAuthentication();
#endif
#if NETFX_CORE
WindowsStoreAuthenticate();
#endif
}
#if NETFX_CORE
static void WindowsStoreAuthenticate()
{
#if UNITY_WP_8_1 && !UNITY_EDITOR
AuthSuccessCallback = _Run_ConnectWithFacebook_SuccessResponse;
AuthFailedCallback = _Run_ConnectWithFacebook_FailedResponse;
UnityEngine.WSA.Application.InvokeOnUIThread(
() =>
{
callback = WebAuthenticationBroker.GetCurrentApplicationCallbackUri();
WebAuthenticationBroker.AuthenticateAndContinue(loginUrl, callback, null, WebAuthenticationOptions.None);
}, true);
#endif
#if UNITY_METRO_8_1 && !UNITY_EDITOR
AuthSuccessCallback = _Run_ConnectWithFacebook_SuccessResponse;
AuthFailedCallback = _Run_ConnectWithFacebook_FailedResponse;
UnityEngine.WSA.Application.InvokeOnUIThread(
async () =>
{
callback = WebAuthenticationBroker.GetCurrentApplicationCallbackUri();
WebAuthenticationResult authResult = await WebAuthenticationBroker.AuthenticateAsync(WebAuthenticationOptions.None, loginUrl);
}, true);
#endif
}
#endif
Note how I use WebAuthenticationBroker.AuthenticateAndContinue for windows phone and WebAuthenticationBroker.AuthenticateAsync for windows 8. This is necessary for their respective platforms. You may want to look into getting the WebAuthenticationBroker which is a Microsoft class that works on WP8 and WSA (im guessing you are aiming for WSA, so am I):-
https://msdn.microsoft.com/library/windows/apps/windows.security.authentication.web.webauthenticationbroker.aspx?f=255&MSPPError=-2147217396
You will also need to build a c# project in unity so you can implement part 2 of it which includes handling when the token details are returned to the app. Here is my code sample of App.xaml.cs which was inspired by https://msdn.microsoft.com/en-us/library/dn631755.aspx:-
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Core;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
using UnityPlayer;
using Template.Common;
using Windows.Security.Authentication.Web;
// The Blank Application template is documented at http://go.microsoft.com/fwlink/?LinkId=234227
namespace Template
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
sealed partial class App : Application
{
private WinRTBridge.WinRTBridge _bridge;
private AppCallbacks appCallbacks;
#if UNITY_WP_8_1
public ContinuationManager continuationManager { get; private set; }
#endif
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
this.InitializeComponent();
appCallbacks = new AppCallbacks();
appCallbacks.RenderingStarted += RemoveSplashScreen;
#if UNITY_WP_8_1
this.Suspending += OnSuspending;
continuationManager = new ContinuationManager();
#endif
}
/// <summary>
/// Invoked when application is launched through protocol.
/// Read more - http://msdn.microsoft.com/library/windows/apps/br224742
/// </summary>
/// <param name="args"></param>
protected override void OnActivated(IActivatedEventArgs args)
{
#if UNITY_WP_8_1
var continuationEventArgs = args as IContinuationActivatedEventArgs;
if (continuationEventArgs != null)
{
ContinueWebAuthentication(args as WebAuthenticationBrokerContinuationEventArgs);
return;
//}
}
#endif
string appArgs = "";
Windows.ApplicationModel.Activation.SplashScreen splashScreen = null;
switch (args.Kind)
{
case ActivationKind.Protocol:
ProtocolActivatedEventArgs eventArgs = args as ProtocolActivatedEventArgs;
splashScreen = eventArgs.SplashScreen;
appArgs += string.Format("Uri={0}", eventArgs.Uri.AbsoluteUri);
break;
}
InitializeUnity(appArgs, splashScreen);
}
#if UNITY_WP_8_1
public void ContinueWebAuthentication(WebAuthenticationBrokerContinuationEventArgs args)
{
WebAuthenticationResult result = args.WebAuthenticationResult;
if (result.ResponseStatus == WebAuthenticationStatus.Success)
{
string responseData = result.ResponseData.Substring(result.ResponseData.IndexOf("access_token"));
String[] keyValPairs = responseData.Split('&');
string access_token = null;
string expires_in = null;
for (int i = 0; i < keyValPairs.Length; i++)
{
String[] splits = keyValPairs[i].Split('=');
switch (splits[0])
{
case "access_token":
access_token = splits[1]; //you may want to store access_token for further use. Look at Scenario5 (Account Management).
break;
case "expires_in":
expires_in = splits[1];
break;
}
}
AppCallbacks.Instance.UnityActivate(Window.Current.CoreWindow, CoreWindowActivationState.CodeActivated);
AppCallbacks.Instance.InvokeOnAppThread(() =>
{
// back to Unity
//function to call or variable that accepts the access token
}, false);
//OutputToken(result.ResponseData.ToString());
//await GetFacebookUserNameAsync(result.ResponseData.ToString());
}
else if (result.ResponseStatus == WebAuthenticationStatus.ErrorHttp)
{
//OutputToken("HTTP Error returned by AuthenticateAsync() : " + result.ResponseErrorDetail.ToString());
AppCallbacks.Instance.InvokeOnAppThread(() =>
{
// back to Unity
//function to call indicating something went wrong
}, false);
}
else if(result.ResponseStatus == WebAuthenticationStatus.UserCancel)
{
//OutputToken("Error returned by AuthenticateAsync() : " + result.ResponseStatus.ToString());
AppCallbacks.Instance.InvokeOnAppThread(() =>
{
// back to Unity
//function to call indicating something went wrong
}, false);
}
}
#endif
/// <summary>
/// Invoked when application is launched via file
/// Read more - http://msdn.microsoft.com/library/windows/apps/br224742
/// </summary>
/// <param name="args"></param>
protected override void OnFileActivated(FileActivatedEventArgs args)
{
string appArgs = "";
Windows.ApplicationModel.Activation.SplashScreen splashScreen = null;
splashScreen = args.SplashScreen;
appArgs += "File=";
bool firstFileAdded = false;
foreach (var file in args.Files)
{
if (firstFileAdded) appArgs += ";";
appArgs += file.Path;
firstFileAdded = true;
}
InitializeUnity(appArgs, splashScreen);
}
/// <summary>
/// Invoked when the application is launched normally by the end user. Other entry points
/// will be used when the application is launched to open a specific file, to display
/// search results, and so forth.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
InitializeUnity(args.Arguments, args.SplashScreen);
}
private void InitializeUnity(string args, Windows.ApplicationModel.Activation.SplashScreen splashScreen)
{
#if UNITY_WP_8_1
ApplicationView.GetForCurrentView().SuppressSystemOverlays = true;
#pragma warning disable 4014
StatusBar.GetForCurrentView().HideAsync();
#pragma warning restore 4014
#endif
appCallbacks.SetAppArguments(args);
Frame rootFrame = Window.Current.Content as Frame;
// Do not repeat app initialization when the Window already has content,
// just ensure that the window is active
if (rootFrame == null && !appCallbacks.IsInitialized())
{
var mainPage = new MainPage(splashScreen);
Window.Current.Content = mainPage;
Window.Current.Activate();
// Setup scripting bridge
_bridge = new WinRTBridge.WinRTBridge();
appCallbacks.SetBridge(_bridge);
#if !UNITY_WP_8_1
appCallbacks.SetKeyboardTriggerControl(mainPage);
#endif
appCallbacks.SetSwapChainPanel(mainPage.GetSwapChainPanel());
appCallbacks.SetCoreWindowEvents(Window.Current.CoreWindow);
appCallbacks.InitializeD3DXAML();
}
Window.Current.Activate();
#if UNITY_WP_8_1
SetupLocationService();
#endif
}
private void RemoveSplashScreen()
{
// This will fail if you change main window class
// Make sure to adjust accordingly if you do something like this
MainPage page = (MainPage)Window.Current.Content;
page.RemoveSplashScreen();
}
#if UNITY_WP_8_1
// This is the default setup to show location consent message box to the user
// You can customize it to your needs, but do not remove it completely if your application
// uses location services, as it is a requirement in Windows Store certification process
private async void SetupLocationService()
{
if (!appCallbacks.IsLocationCapabilitySet())
{
return;
}
const string settingName = "LocationContent";
bool userGaveConsent = false;
object consent;
var settings = Windows.Storage.ApplicationData.Current.LocalSettings;
var userWasAskedBefore = settings.Values.TryGetValue(settingName, out consent);
if (!userWasAskedBefore)
{
var messageDialog = new Windows.UI.Popups.MessageDialog("Can this application use your location?", "Location services");
var acceptCommand = new Windows.UI.Popups.UICommand("Yes");
var declineCommand = new Windows.UI.Popups.UICommand("No");
messageDialog.Commands.Add(acceptCommand);
messageDialog.Commands.Add(declineCommand);
userGaveConsent = (await messageDialog.ShowAsync()) == acceptCommand;
settings.Values.Add(settingName, userGaveConsent);
}
else
{
userGaveConsent = (bool)consent;
}
if (userGaveConsent)
{ // Must be called from UI thread
appCallbacks.SetupGeolocator();
}
}
#endif
/// <summary>
/// Invoked when Navigation to a certain page fails
/// </summary>
/// <param name="sender">The Frame which failed navigation</param>
/// <param name="e">Details about the navigation failure</param>
void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
{
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
}
/// <summary>
/// Invoked when application execution is being suspended. Application state is saved
/// without knowing whether the application will be terminated or resumed with the contents
/// of memory still intact.
/// </summary>
/// <param name="sender">The source of the suspend request.</param>
/// <param name="e">Details about the suspend request.</param>
private async void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
await SuspensionManager.SaveAsync();
deferral.Complete();
}
}
}
You will also need the ContinuationManager.cs class and the SuspensionManager.cs class but mainly for WP8.1 Universal. This isnt needed for W8.1 and so you should ensure that you are using #defines to keep them within their own contexts
If you want, instead of calling on WebAuthenticationBroker, you could call on the facebook app directly but I don't know of all the details. You can read up on that on facebook's website developers.facebook.com/docs/facebook-login/login-for-windows-phone. That method isnt recommended incase the user doesn't have it installed.
Long (rushed) story short, call WebAuthenticationBroker, handle the continuation Event in OnActivated event to catch the WebAuthenticationBrokerContinuationEventArgs object coming through and use the data however you see fit. Be sure to use the following if you want to call any code in the unity side. Note that you can directly access the c# code from App.xaml.cs:-
AppCallbacks.Instance.InvokeOnAppThread(() =>
{
// back to Unity
//your code here
}, false);
Also note that this is mainly to get the access token. Once we have that, we can make basic WWW calls directly to facebook and get data back from it. The data will be returned as a JSON format (ps this is such an awesome clean format!) you can use the .NET json library to serialize it into a class. I use json2csharp.com to convert any example output into a class which I just parse into the json library.