I have a code here which automatically plays a sound when target is found and stops when target is lost. What I'm doing is that I want to repeat the sound of the target currently found. The problem is when i click the replay button, the sound of the last target found, and the current target are both playing.
I coded this on the DefaultTrackableEventHandler script. Here's my code:
public AudioSource soundTarget;
public AudioClip clipTarget;
private AudioSource[] allAudioSources;
public Button Button;
void StopAllAudio()
{
allAudioSources = FindObjectsOfType(typeof(AudioSource)) as AudioSource[];
foreach (AudioSource audioS in allAudioSources)
{
audioS.Stop();
}
}
void playSound(string ss)
{
clipTarget = (AudioClip)Resources.Load(ss);
soundTarget.clip = clipTarget;
soundTarget.loop = false;
soundTarget.playOnAwake = false;
soundTarget.Play();
}
public void ReplayAudio()
{
soundTarget.PlayOneShot(clipTarget);
}
Under public virtual void OnTrackingFound()
public virtual void OnTrackingFound()
{
if (mTrackableBehaviour.TrackableName == "letterA")
{
playSound("sounds/airplane");
Button.onClick.AddListener(ReplayAudio);
}
if (mTrackableBehaviour.TrackableName == "letterB")
{
playSound("sounds/banana");
Button.onClick.AddListener(ReplayAudio);
}
//On trackingLost: StopAllAudio();
You need to call RemoveAllListeners on your button before adding another listener or they will stack everytime a target is found.
public virtual void OnTrackingFound()
{
Button.onClick.RemoveAllListeners();
if (mTrackableBehaviour.TrackableName == "letterA")
{
playSound("sounds/airplane");
Button.onClick.AddListener(ReplayAudio);
}
if (mTrackableBehaviour.TrackableName == "letterB")
{
playSound("sounds/banana");
Button.onClick.AddListener(ReplayAudio);
}
}
Related
I have three buttons, three players, and I want to know which player clicked on which button by assigning them a different popup UI color. Let's say player 1- red, player 2 - green, player 3 - white. So when player 1 clicks on button(1), the red panel will be visible for all other players telling them who did that.
Possible solution: I have been advised to call to the Host with a Cmd method and pass the connectionId of the player who click on it. Then check the list of players on the NetworkManager, and find the matching player object. Once it has that player object, it would get the name and Id, and calls a RPC to the clients to show the UI element, passing them the correct text and color.
What I Have: I have list of my players:
public List<MyNetworkPlayer> players { get; } = new List<MyNetworkPlayer>();
and i can use my player by
MyNetworkPlayer player = conn.identity.GetComponent<MyNetworkPlayer>();
I also can attatch my NetworkManager if that can help:
public class MyNetworkManager : NetworkManager
{
public static event Action ClientOnConnected;
public static event Action ClientOnDisconnected;
// players should't be able to join the lobby during the game.
private bool isGameInProgress = false;
// here, we create a list of players, in order to display them in the lobby later.
public List<MyNetworkPlayer> players { get; } = new List<MyNetworkPlayer>();
#region Serwer
public override void OnServerConnect(NetworkConnection conn )
{ //kick a player if the game is in progress
if (!isGameInProgress) { return; }
base.OnServerConnect (conn);
conn.Disconnect();
}
// here i write some own logic about what heppens when a client connects to a server. If the connection is successful
// the message about connection will be display in a console!
public override void OnServerDisconnect(NetworkConnection conn)
{ // when the server disconnects someone
// lets grab that player
MyNetworkPlayer player = conn.identity.GetComponent<MyNetworkPlayer>();
players.Remove(player);
base.OnServerDisconnect(conn);
}
public override void OnStopServer()
//what happens when we stop running the server
{
players.Clear();
isGameInProgress = false;
}
public void StartGame()
{ // the game won't start with the less than 2 players
if(players.Count <2){return;}
//if we have more than 2 we can start the game
isGameInProgress = true;
//also we change our scene here
ServerChangeScene("GameScene");
}
public override void OnServerAddPlayer(NetworkConnection conn)
{
base.OnServerAddPlayer(conn);
//reference to network player
MyNetworkPlayer player = conn.identity.GetComponent<MyNetworkPlayer>();
players.Add(player);
// here I assign player's name base on joining order
player.SetDisplayName($"Player {players.Count}");
Debug.Log("Player conn ID: " + conn.connectionId);
//here we dclare who will be a party owner
// if there is only one player he will be a party owner
player.SetPartyOwner(players.Count == 1);
Debug.Log($"There are now {numPlayers} players");
}
//when a client stops, we shold also clear the player's list and currently we only
// add to it on the server
public override void OnStopClient()
{
players.Clear();
}
#endregion
#region Client
[System.Obsolete]
public override void OnClientConnect(NetworkConnection conn)
{
base.OnClientConnect(conn);
Debug.Log("Client connected: " + conn.connectionId);
ClientOnConnected?.Invoke();
}
[System.Obsolete]
public override void OnClientDisconnect(NetworkConnection conn)
{
base.OnClientDisconnect(conn);
ClientOnDisconnected?.Invoke();
}
#endregion
}
And MyNetworkPlayer:
public class MyNetworkPlayer : NetworkBehaviour
{
//Here i refer to a player text and change it based on current player number.
[SerializeField] private TMP_Text displayNameText= null;
[SyncVar(hook = nameof(AuthorityHandlePartyOwnerStateUpdated))]
private bool isPartyOwner = false;
// syncvar that will allow other players to store our name
[SyncVar(hook = nameof(ClientHandleDisplayNameUpdated))]
public string displayName;
// this below is the event created in order to handle the changes of client info
// like it's colour and so on
public static event Action ClientOnInfoUpdated;
//public string which returns the display name
public string GetDisplayName()
{
return displayName;
}
public bool GetIsPartyOwner()
{
return isPartyOwner;
}
public static event Action<bool> AuthorityOnPartyOwnerStateUpdated;
#region Serwer
public override void OnStartServer()
{
DontDestroyOnLoad(gameObject);
}
[Server]
// checking who the party owner is
public void SetPartyOwner(bool state)
{
isPartyOwner = state;
}
[Server]
public void SetDisplayName(string newDisplayName)
{
displayName = newDisplayName;
}
#endregion
#region Commands
[Command]
private void CmdSetDisplayName(string newDisplayName)
{
//here I add the codition saying that the player length can not be shorter than 2 characters
//if(newDisplayName.Length <2 || newDisplayName.Length > 20){ return; }
RpcLogNewName(newDisplayName);
SetDisplayName(newDisplayName);
}
[Command]
// A command so client can tell the server that he wants to start the game
public void CmdStartGame()
{
if(!isPartyOwner) { return; }
//otherwise tell the NetworkManager to stop the game
((MyNetworkManager)NetworkManager.singleton).StartGame();
}
#endregion
#region Authorities
private void AuthorityHandlePartyOwnerStateUpdated(bool oldState, bool newState)
{
// it allows you to change who the party owner is during the lobby
// its goinna be a button to change the authority of the ownershit
if(!hasAuthority) {return; }
// this down below is for the UI button change its statement
AuthorityOnPartyOwnerStateUpdated?.Invoke(newState);
}
#endregion
#region Client
// Here we change the player's name
private void HandleDisplayNameUpdated(string oldName, string newName)
{
displayNameText.text = newName;
}
// Here we connect the reference from MyNetworkManager
public override void OnStartClient()
{
if (NetworkServer.active) {return;}
// whenever a client starts and we're not the host, if we just end up a client, we can
// be added to our list of the players
((MyNetworkManager)NetworkManager.singleton).players.Add(this);
// this below prevcents our player objects from being destroyed when
// changing the scanes
DontDestroyOnLoad(gameObject);
}
private void ClientHandleDisplayNameUpdated(string oldDisplayName, string newDisplayName)
{ // we need to display people's name when they got updated (in the UI)
ClientOnInfoUpdated?.Invoke();
}
public override void OnStopClient()
{
ClientOnInfoUpdated?.Invoke();
//if we are not the server we do this for everyone:
if (!isClientOnly) {return; }
// Here we remove a player from the list of players
((MyNetworkManager)NetworkManager.singleton).players.Remove(this);
//if we are the server we do this for everyone:
if (!hasAuthority) { return; }
}
[ClientRpc]
//rpc log new name is one of our new names being set
private void RpcLogNewName(string newDisplayName)
{
Debug.Log(newDisplayName);
}
#endregion
}
My problem: As I said in possible solution, I don't know how to send connectionId to host and then, based on that, assign corresponding UI element for each different player. I would be really thankful for any advise on how to code that.
I used the code from Unity and the rewards are multiples... first 1 then 2 then 3 and increasing... i tried deleting some code, but keep doing the same, then this happened.
I searched online and i couldn't find anything that explains clicking the rewarded ads button more than once.
Everything (apparently) is working fine only if i don't assing a button and leave the "SerializeField" empty, because if i remove the button, goes back to give more rewards... can somebody check this and tell me what's going on? i add the code Here
using UnityEngine;
using UnityEngine.Advertisements;
using UnityEngine.UI;
public class AdsInitializer : MonoBehaviour, IUnityAdsInitializationListener, IUnityAdsLoadListener, IUnityAdsShowListener
{
[SerializeField] string _androidGameId = "4634758";
[SerializeField] string _iOSGameId = "4634759";
[SerializeField] bool _testMode = false;
private string _gameId;
[SerializeField] Button _showAdButton;
[SerializeField] string rewardAndroidAdUnitId = "Rewarded_Android";
[SerializeField] string rewardiOSAdUnitId = "Rewarded_iOS";
string rewardAdUnitId = null; // This will remain null for unsupported platforms
void Awake()
{
_gameId = (Application.platform == RuntimePlatform.IPhonePlayer)
? _iOSGameId
: _androidGameId;
rewardAdUnitId = (Application.platform == RuntimePlatform.IPhonePlayer)
? rewardiOSAdUnitId
: rewardAndroidAdUnitId;
Advertisement.Initialize(_gameId, _testMode, this);
rewardAdUnitId = (Application.platform == RuntimePlatform.IPhonePlayer)
? rewardiOSAdUnitId
: rewardAndroidAdUnitId;
}
public void OnInitializationComplete()
{
Debug.Log("Unity Ads initialization complete.");
LoadRewardedAd();
}
public void OnInitializationFailed(UnityAdsInitializationError error, string message)
{
Debug.Log($"Unity Ads Initialization Failed: {error.ToString()} - {message}");
}
#region REWARDED ADS
public void LoadRewardedAd()
{
Debug.Log("Loading Ad: " + rewardAdUnitId);
Advertisement.Load(rewardAdUnitId, this);
}
public void ShowRewardedAd()
{
Advertisement.Show(rewardAdUnitId, this);
}
#endregion
public void OnUnityAdsAdLoaded(string adUnitId)
{
if (adUnitId.Equals(rewardAdUnitId))
{
// Configure the button to call the ShowAd() method when clicked:
//_showAdButton.onClick.AddListener(ShowRewardedAd);
// Enable the button for users to click:
//_showAdButton.interactable = true;
Debug.Log("RewardedAds Loaded");
}
}
public void OnUnityAdsShowComplete(string adUnitId, UnityAdsShowCompletionState showCompletionState)
{
if (adUnitId.Equals(rewardAdUnitId) && showCompletionState.Equals(UnityAdsShowCompletionState.COMPLETED))
{
Debug.Log("Unity Ads Rewarded Ad Completed");
// Grant a reward.
_showAdButton.onClick.RemoveAllListeners(); //with this line of code the problem is solved but shows the NullReference.
// Load another ad:
Advertisement.Load(rewardAdUnitId, this);
}
}
public void OnUnityAdsFailedToLoad(string adUnitId, UnityAdsLoadError error, string message)
{
if (adUnitId.Equals(rewardAdUnitId))
{
Debug.Log($"Error loading Ad Unit {adUnitId}: {error.ToString()} - {message}");
}
}
public void OnUnityAdsShowFailure(string adUnitId, UnityAdsShowError error, string message)
{
if (adUnitId.Equals(rewardAdUnitId))
{
Debug.Log($"Error showing Ad Unit {adUnitId}: {error.ToString()} - {message}");
}
}
public void OnUnityAdsShowStart(string adUnitId) { }
public void OnUnityAdsShowClick(string adUnitId) { }
void OnDestroy()
{
//with or without it doesn't change, it works only when the game is closed, and when reopen is working the same
//_showAdButton.onClick.RemoveAllListeners();
}
}
SOLVED!!
So, basically the script from Unity somehow has an unknown error, i couldn't explain myself how or from where... so the solution i fould was to add a bool in this case showAd = false and implemented it inside the showAd and the OnUnityAdsShowComplete functions, fortunatelly that was enough to solve the issue, now I can put the script in the button or in an AdManager and call the funtion from the button in the section OnClick() either way now is not showing neither error neither multiples with the rewards.
Hope it will be usefull for someone else.
using UnityEngine;
using UnityEngine.Advertisements;
using UnityEngine.UI;
public class AdsInitializer : MonoBehaviour, IUnityAdsInitializationListener, IUnityAdsLoadListener, IUnityAdsShowListener
{
[SerializeField] string _androidGameId = "4634758";
[SerializeField] string _iOSGameId = "4634759";
[SerializeField] bool _testMode = true;
private string _gameId;
[SerializeField] Button _showAdButton; //You can remove this if want to add the function manually from OnClick()
[SerializeField] string _androidAdUnitId = "Rewarded_Android";
[SerializeField] string _iOSAdUnitId = "Rewarded_iOS";
string _adUnitId = null; // This will remain null for unsupported platforms
private bool showAd = false;
void Awake()
{
InitializeAds();
Debug.Log("Awake");
_adUnitId = (Application.platform == RuntimePlatform.IPhonePlayer)
? _iOSAdUnitId
: _androidAdUnitId;
Debug.Log("the _adUnitId is: " + _adUnitId);
}
public void InitializeAds()
{
_gameId = (Application.platform == RuntimePlatform.IPhonePlayer)
? _iOSGameId
: _androidGameId;
Advertisement.Initialize(_gameId, _testMode, this);
}
public void OnInitializationComplete()
{
Debug.Log("Unity Ads initialization complete.");
LoadAd();
}
public void OnInitializationFailed(UnityAdsInitializationError error, string message)
{
Debug.Log($"Unity Ads Initialization Failed: {error.ToString()} - {message}");
}
public void LoadAd()
{
// IMPORTANT! Only load content AFTER initialization (in this example, initialization is handled on top of the script).
Debug.Log("Loading Ad: " + _adUnitId);
Advertisement.Load(_adUnitId, this);
}
// If the ad successfully loads, add a listener to the button and enable it:
public void OnUnityAdsAdLoaded(string adUnitId)
{
Debug.Log("Ad Loaded: " + adUnitId);
if (adUnitId.Equals(_adUnitId))
{
// Configure the button to call the ShowAd() method when clicked:
_showAdButton.onClick.AddListener(ShowAd); //You can remove this if want to add the function manually from OnClick()
// Enable the button for users to click:
_showAdButton.interactable = true; //You can remove this if want to add the function manually from OnClick()
}
}
// Implement a method to execute when the user clicks the button:
public void ShowAd()
{
if (showAd == false)
{
Debug.Log("Showing Ad");
// Disable the button:
_showAdButton.interactable = false; //You can remove this if want to add the function manually from OnClick()
// Then show the ad:
Advertisement.Show(_adUnitId, this);
_showAdButton.onClick.RemoveAllListeners(); //You can remove this if want to add the function manually from OnClick()
Debug.Log("All Listeners Removed");
showAd = true;
}
}
// Implement the Show Listener's OnUnityAdsShowComplete callback method to determine if the user gets a reward:
public void OnUnityAdsShowComplete(string adUnitId, UnityAdsShowCompletionState showCompletionState)
{
if (showAd == true)
{
if (adUnitId.Equals(_adUnitId) && showCompletionState.Equals(UnityAdsShowCompletionState.COMPLETED))
{
Debug.Log("Unity Ads Rewarded Ad Completed");
// Grant a reward.
// Load another ad:
Advertisement.Load(_adUnitId, this);
showAd = false;
}
}
}
public void OnUnityAdsFailedToLoad(string adUnitId, UnityAdsLoadError error, string message)
{
Debug.Log($"Error loading Ad Unit {adUnitId}: {error.ToString()} - {message}");
// Use the error details to determine whether to try to load another ad.
}
public void OnUnityAdsShowFailure(string adUnitId, UnityAdsShowError error, string message)
{
Debug.Log($"Error showing Ad Unit {adUnitId}: {error.ToString()} - {message}");
// Use the error details to determine whether to try to load another ad.
}
public void OnUnityAdsShowStart(string adUnitId) { }
public void OnUnityAdsShowClick(string adUnitId) { }
}
i'm trying to make a simple AR app, the flow of the app i try to make is, camera detect first marker > do something, camera detect 2nd marker > do something different
the problem is i already googled with combination of keyword i can think of, and also i also look for the documentation but nothing really i can found that help with my problem
my initial though is to just use a script on the said marker and put the "do something" on void OnEnable, but the object is enable anyway, so its useless
anyone can lend me a rope ?
DefaultTrackingEventHandler is the script handling the tracking. In the sample scene for multiple markes, it is attached to ImageTarget.
That script registers to the TrackableBehaviour and propagates the tracking found/lost via the OnTrackingFound/Lost methods.
In the sample scene, those methods collect the collider and renderer of the child object and enables/disables.
What you can do is to propagate further the info to other listeners. In this case you could enable a script that runs an update checking for a condition until it is met and disable the script and matching trackable behaviour.
public class DefaultTrackableEventHandler : MonoBehaviour,
ITrackableEventHandler
{
private TrackableBehaviour mTrackableBehaviour;
[SerializedField] private MyConditionClass actionMb = null;
protected virtual void Start()
{
mTrackableBehaviour = GetComponent<TrackableBehaviour>();
if (mTrackableBehaviour)
{
mTrackableBehaviour.RegisterTrackableEventHandler(this);
}
}
public void OnTrackableStateChanged(
TrackableBehaviour.Status previousStatus,
TrackableBehaviour.Status newStatus)
{
if (newStatus == TrackableBehaviour.Status.DETECTED ||
newStatus == TrackableBehaviour.Status.TRACKED ||
newStatus == TrackableBehaviour.Status.EXTENDED_TRACKED)
{
OnTrackingFound();
}
else
{
OnTrackingLost();
}
}
protected virtual void OnTrackingFound()
{
// Was already successfully done
if(this.actionMb.ConditionMet == true){ return; }
this.actionMb.enable = true;
}
protected virtual void OnTrackingLost()
{
// Was already successfully done
if(this.actionMb.ConditionMet == true){ return; }
this.actionMb.enable = false;
}
}
public abstract class MyConditionClass : MonoBehaviour
{
public bool ConditionMet{ get; private set; }
protected abstract bool CheckCondition();
protected virtual void Update(){
if(ConditionMet == true){ return; }
ConditionMet = CheckCondition();
}
}
public class MyConditionClassForInput : MyConditionClass
{
protected override bool CheckCondition(){
return (Input.GetKeyDown(KeyCode.Space));
}
}
So you have a base class containing the data that should be common to all conditions, like whether the condition was already met as shown. and the Update running a condition check.
Then you have sub class that implements the condition check (has to since abstract).
Starting in 8.6.7, Vuforia have refactored ITrackableEventHandler into a base class DefaultTrackableEventHandler instead of an interface.
public class MyTrackableEventHandler : DefaultTrackableEventHandler
{
...
}
Further, the callbacks RegisterTrackableEventHandler and UnregisterTrackableEventHandler also need to be updated. This now uses a callback method.
private void OnEnable()
{
_trackableBehaviour = GetComponent<TrackableBehaviour>();
_trackableBehaviour.RegisterOnTrackableStatusChanged(OnTrackableStateChanged);
}
private void OnDisable()
{
_trackableBehaviour.RegisterOnTrackableStatusChanged(OnTrackableStateChanged);
}
public void OnTrackableStateChanged(StatusChangeResult obj)
{
if (obj.NewStatus == Status.DETECTED || obj.NewStatus == Status.TRACKED
|| obj.NewStatus == Status.EXTENDED_TRACKED)
{
OnTrackingFound();
}
else
OnTrackingLost();
}
I read a lot of documentation regarding this problem, but I don't seen any answer.
I have a Xamarin form app that play mp3 sample music from the Groove music service.
On Android, everyhting workd fine.
On Ios, I'm not able to play the sound. I get the URL and it's not possible to ear anything (I was able to play a local mp3)
I see this problem occured also for people using SWIFT : How to play MP3 From URL in iOS
I also found several exemple on the Xamarin forum but no sound neither: https://forums.xamarin.com/discussion/19883/how-do-i-get-the-avplayer-to-play-an-mp3-from-a-remote-url
Here is the code I use:
public class AudioService : IAudio
{
protected string FileName = string.Empty;
protected AVPlayer Player;
public bool IsPlaying { get; private set; }
public void Init(string fileName)
{
//FileName = fileName;
string second = "http://progdownload.zune.net/145/119/034/170/audio.mp3?rid=0b80911a-ba3b-42ec-b17f-c242ba087024-s4-nl-BE-music-asset-location";
FileName = second;
QueueFile();
}
public async void PlayStream(string uri)
{
Init(uri);
System.Diagnostics.Debug.WriteLine("Enter in function");
Player.Play();
System.Diagnostics.Debug.WriteLine("Play sound");
System.Diagnostics.Debug.WriteLine(FileName);
IsPlaying = true;
System.Diagnostics.Debug.WriteLine(IsPlaying);
}
public void Pause()
{
IsPlaying = false;
Player.Pause();
}
public void Stop()
{
IsPlaying = false;
if (Player == null) return;
Player.Dispose();
Player = null;
}
public bool HasFile
{
get { return Player != null; }
}
private void QueueFile()
{
if (string.IsNullOrWhiteSpace(FileName))
{
throw new Exception("No file specified to play");
}
using (var url = NSUrl.FromString(FileName))
{
var test = AVAsset.FromUrl(url);
var playerItem = AVPlayerItem.FromAsset(test);
// if Player is null, we're creating a new instance and seeking to the spot required
// otherwise we simply resume from where we left off.
if (Player == null)
{
Player = AVPlayer.FromPlayerItem(playerItem);
if (Player == null)
{
// todo: what should we do if the file doesn't exist?
return;
}
}
}
}}
(IAudio just implements playstream and stop)
If you click on the url, your browser will be able to play music
http://progdownload.zune.net/145/119/034/170/audio.mp3?rid=0b80911a-ba3b-42ec-b17f-c242ba087024-s4-nl-BE-music-asset-location
Does any one was able to play mp3 from internet
The answer in the info.list file. You need to add this to your info.list:
<key>NSAppTransportSecurity</key><dict>
<key>NSAllowsArbitraryLoads</key>
<true/></dict>
I had the same issue and resolved it.
I've made some cleaning for your code :)
This is what we got, including the detecting the end of playing
public class AudioService
{
#region Members
protected AVPlayer Player;
#endregion
#region Properties
public bool IsPlaying { get; private set; }
public bool HasFile => Player != null;
public Action MediaEnded { get; set; }
#endregion
#region Public Methods
public void PlayStream(string uri)
{
try
{
if (Player != null)
Player.Dispose();
Player = null;
QueueFile(uri);
Player.Play();
IsPlaying = true;
}
catch (Exception ex)
{
IsPlaying = false;
Crashes.TrackError(ex);
}
}
public void Pause()
{
IsPlaying = false;
Player.Pause();
}
public void Stop()
{
IsPlaying = false;
if (Player == null)
return;
Player.Dispose();
Player = null;
}
public void QueueFile(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
{
return;
}
using (var url = NSUrl.FromString(fileName))
{
var asset = AVAsset.FromUrl(url);
var playerItem = AVPlayerItem.FromAsset(asset);
if (Player == null)
{
Player = AVPlayer.FromPlayerItem(playerItem);
if (Player == null)
{
return;
}
else
{
NSNotificationCenter.DefaultCenter.AddObserver(AVPlayerItem.DidPlayToEndTimeNotification, OnPlayerDidFinishPlaying, NSObject.FromObject(playerItem));
}
}
}
}
#endregion
#region Private Methods
private void OnPlayerDidFinishPlaying(NSNotification notification)
{
IsPlaying = false;
MediaEnded?.Invoke();
}
#endregion
I'm creating a Media player application with Unity3D and C#.
(My question is not related to Unity, it's a pure design problem)
Here is what I currently have:
an IApp interface, with implementers:
TextViewer
ImageViewer
MediaPlayer
an IFile interface, with implementers:
TextFile
ImageFile
MediaFile - with children:
VideoFile
AudioFile
Here's the interface:
public interface IApp
{
void Open(IFile file);
Type SupportedType { get; }
}
Each app has a specific supported file type it could open.
A word about my MediaPlayer, is that it opens/plays both Audio and Video files. But the way I open videos, is different from the way I open audios, so there's a unique logic for each.
Now here's the code - very simple (but not fully implemented yet):
public class MediaPlayer : IApp
{
public Type SupportedType { get { return typeof(MediaFile); } }
public void Open(IFile file)
{
if (file is MediaFile)
Console.WriteLine("MediaPlayer opening media file...");
}
List<MediaFile> Medias = new List<MediaFile>();
public MediaFile Current { private set; get; }
public PlaybackControls Controls { private set; get; }
public PlaybackSettings Settings { private set; get; }
public MediaPlayer()
{
Controls = new PlaybackControls(this);
Settings = new PlaybackSettings(this);
}
public class PlaybackControls
{
private MediaPlayer player;
private int index;
public PlaybackControls(MediaPlayer player)
{
this.player = player;
}
public void Seek(float pos) { }
public void Next()
{
index = (index + 1) % player.Medias.Count;
player.Current = player.Medias[index];
}
public void Previous()
{
index--;
if (index < 0)
index = player.Medias.Count - 1;
player.Current = player.Medias[index];
}
private void PlayVideo(VideoFile video)
{
// video logic
}
private void PlayAudio(AudioFile audio)
{
// audio logic
}
public void Play(MediaFile media)
{
IsPlaying = true;
if (media is AudioFile)
PlayAudio(media as AudioFile);
else if (media is VideoFile)
PlayVideo(media as VideoFile);
}
public void Play()
{
Play(player.Current);
}
public void Pause()
{
IsPlaying = false;
}
public void Stop()
{
IsPlaying = false;
Seek(0);
}
public bool IsPlaying { get; private set; }
}
public class PlaybackSettings
{
// Volume, shuffling, looping, etc
}
}
The thing that I don't quite like, is the Play(Media) method. Inside, I'm doing a check upon the media type, and depending on whether the media is a video or audio, I'm calling the right method. I don't like that, I don't feel it's quite right. What if I had other types of media, like picture? what if I wanted to move ImageFile under MediaFile?
I would then have to add another else-if statement, which isn't polymorphic at all.
What I could do instead, is let the media files choose what method to call, like:
public abstract class MediaFile : IFile
{
//...
public abstract void Open(MediaPlayer from);
//...
}
public class AudioFile : MediaFile
{
public override void Open(MediaPlayer from)
{
from.PlayAudio(this);
}
}
public class VideoFile : MediaFile
{
public override void Open(MediaPlayer from)
{
from.PlayVideo(this);
}
}
Now in the MediaPlayer:
public void Open(MediaFile media)
{
media.Open(this); // polymorphically open it
}
No more else-if, nice! But this introduces other inconveniences I don't like:
VideoFile & MediaPlayer and AudioFile & MediaPlayer are now more tightly coupled.
There's now a circular dependency (MediaPlayer has to know about Audio/VideoFile and vise versa)
I don't think it makes sense for the Audio/VideoFiles to be able to open themselves, by themselves (although they're not really doing so, they're just telling the MediaPlayer how to open them. The MediaPlayer should know how to, he doesn't need anyone telling him how to do his work.)
It feels very redundant, it's like asking somebody to point at his ear, so he wraps his right hand around his head, and points to his left ear instead of right! - What's happening is that we're going:
either
MediaPlayer.Open(Media) -> AudioFile.Open(AudioFile) -> MediaPlayer.OpenAudio(AudioFile)
or
MediaPlayer.Open(Media) -> VideoFile.Open(VideoFile) -> MediaPlayer.OpenVideo(VideoFile)
We're circling around ourselves, in the name of polymorphism where we could have just gone directly to the right methods.
I think both the two above approaches are not best, but if I were to choose one, I would go for the first.
What do you think? Is there a better way? - A nice, elegant, robust polymorphic way that shoots all birds with one stone? How should I have gone about this? Maybe there's a design pattern I could use here?
And please correct me if I was wrong in my judgement.
Thanks a lot for any help in advance.
You have couple options.
1) Use dictionary of delegates and select based on file type, which delegate to run:
public class PlaybackControls
{
private MediaPlayer player;
private int index;
Dictionary<string, Action<MediaFile>> _fileActionMethods;
public PlaybackControls(MediaPlayer player)
{
this.player = player;
_fileActionMethods = new Dictionary<string, Action<MediaFile>>();
_fileActionMethods.Add(typeof(VideoFile).Name, x => PlayVideoFile(x));
_fileActionMethods.Add(typeof(AudioFile).Name, x => PlayAudioFile(x));
}
public void Seek(float pos) { }
public void Next()
{
index = (index + 1) % player.Medias.Count;
player.Current = player.Medias[index];
}
public void Previous()
{
index--;
if (index < 0)
index = player.Medias.Count - 1;
player.Current = player.Medias[index];
}
public void Play(MediaFile media)
{
IsPlaying = true;
_fileActionMethods[media.GetType().Name](media);
}
public void Play()
{
Play(player.Current);
}
public void Pause()
{
IsPlaying = false;
}
public void Stop()
{
IsPlaying = false;
Seek(0);
}
public bool IsPlaying { get; private set; }
private void PlayVideoFile(MediaFile file) { }
private void PlayAudioFile(MediaFile file) { }
}
2) Second option is based on similar concept of dynamic selection, but uses another layer of abstraction that enables you to handle each file using separate class. For lack of imagination I am naming it IFileActionHandler. It has only one method now but you can add more, if you need to. The sample below shows how to dynamically select the correct implementation based on the file type. I created a dictionary of these implementations in the constructor. Depending on how large the memory footprint of the implementations is, you may want to think about another approach - define the key value pairs in a static file (XML, config, txt, whatever) and create the correct instance using one of System.Acticator.CreateInstance overloads.
interface IFileActionHandler
{
void PlayFile(IFile file);
}
class FileActionHandlerBase : IFileActionHandler
{
IApp _app;
public FileActionHandlerBase(IApp app) // It may not be needed depending on what you want to do.
{
_app = app;
}
public abstract void PlayFile(IFile file);
}
class AudioFileActionHandler : FileActionHandlerBase
{
public AudioFileActionHandler(IApp app)
: base(app) { }
public override void PlayFile(IFile file)
{
// Your implementation...
}
}
class VideoFileActionHandler : FileActionHandlerBase
{
public VideoFileActionHandler(IApp app)
: base(app) { }
public override void PlayFile(IFile file)
{
// Your implementation...
}
}
public class PlaybackControls
{
private MediaPlayer player;
private int index;
Dictionary<string, IFileActionHandler> _fileActionHandlers;
public PlaybackControls(MediaPlayer player)
{
this.player = player;
_fileActionHandlers = new Dictionary<string, IFileActionHandler>();
_fileActionHandlers.Add(typeof(VideoFile).Name, new VideoFileActionHandler(player));
_fileActionHandlers.Add(typeof(AudioFile).Name, new AudioFileActionHandler(player));
}
public void Seek(float pos) { }
public void Next()
{
index = (index + 1) % player.Medias.Count;
player.Current = player.Medias[index];
}
public void Previous()
{
index--;
if (index < 0)
index = player.Medias.Count - 1;
player.Current = player.Medias[index];
}
public void Play(MediaFile media)
{
IsPlaying = true;
_fileActionHandlers[media.GetType().Name].PlayFile(media);
}
public void Play()
{
Play(player.Current);
}
public void Pause()
{
IsPlaying = false;
}
public void Stop()
{
IsPlaying = false;
Seek(0);
}
public bool IsPlaying { get; private set; }
}