I began coding in c# only a week ago so sorry if any questions are easily solvable.
I'm trying to make a multiplayer game of tag, and so far tagging works. What I'm dealing with is trying to reset the game so it can restart. I'm counting the amount of tagged players by using the players PhotonView with Tag.Length
Variables
private GameObject[] UnTaggedPlayers;
public PhotonView Player;
Code
public void OnTagged()
{
//Flag as tagged
_isTagged = true;
//Touchbacks countdown
_touchbackCountDown = _touchBackDuration;
GetComponentInChildren<SkinnedMeshRenderer>().material = _InfectionMat;
tagged.Play();
Player.tag = "Tagged";
}
[PunRPC]
public void OnUnTagged()
{
//Flag as tagged
_isTagged = false;
//Change the color of player
GetComponentInChildren<SkinnedMeshRenderer>().material = _initialMat;
GetComponentInChildren<SkinnedMeshRenderer>().material.color = _initialColor;
Player.tag = "NotTagged";
}
public void Update()
{
UnTaggedPlayers = GameObject.FindGameObjectsWithTag("NotTagged");
if (photonView.IsMine)
{
if (_touchbackCountDown > 0f)
{
_touchbackCountDown -= Time.deltaTime;
}
}
}
private void OnTriggerEnter(Collider other)
{
{
var otherPlayer = other.GetComponentInParent<TagManager>();
if (otherPlayer != null)
if (_isTagged && _touchbackCountDown <= 0f)
{
if(UnTaggedPlayers.Length <= 0)
{
//Untag Everyone
}
else
{
otherPlayer.photonView.RPC("OnTagged", RpcTarget.AllBufferedViaServer);
}
}
}
}
}
I'm trying to fill in the part with //Untag everyone, But I'm unsure how to do so. I was trying to find a way to find everyones photonView and send it an RPC to untag.
Using PunRPC is a good idea.
You could declare a List<PhotonView> playerList and add all the instantiated players.
With this list with all the photonViews of all the players, just scroll down the list and invoke the RPC method for each one.
for (int i = 0; i < PhotonNetwork.PlayerList.Length; i++)
playerList.RPC("OnTagged", RpcTarget.AllBufferedViaServer);
Alternatively, other players could also be registered in a static list to save resources, which would be filled by all players.
To call your function on all it would be enough to call the "UntagEvryPlayer ()" RPC method on your player.
The called method can simply go through the static list of players and call the OnUnTagged () method on each one locally.
It would have the advantage that instead of calling the RPC method on all the photoViews and thus creating many calls, exponentially with respect to the number of players, with the second solution you would send the RPC event only with your photonView and the other remote clients would call the local method.
If you need more help just ask :)
Related
so I'm wanting to pause the game once the amount of enemies hits 0. So I'm using GameObject.FindGameObjectsWithTag("Enemy").Length to find the number of enemies. I put this in a function that's called right when the enemies are instantiated so I can see the length go to 4, as there's 4 enemies spawning. When an enemy is killed the function is called again where the length is printed to console again. For some reason, on the first enemy killed the count repeats with a 4 again despite there only being 3 enemies. Once another enemy is killed it reports 3 when there's actually 2 and so on until I get to 1 when there's 0 enemies.
Here's the first snippet of code:
public class EnemyList : MonoBehaviour
{
public List<GameObject> weakMobs = new List<GameObject>();
public List<GameObject> mediumMobs = new List<GameObject>();
public List<GameObject> bossMobs = new List<GameObject>();
public List<Transform> spawningChildren = new List<Transform>();
public static int mobCount;
void Start()
{
for (int i = 0; i < spawningChildren.Count; i++)
{
GameObject newWeakMob = Instantiate(weakMobs[0], spawningChildren[Random.Range(0, 4)]) as GameObject;
}
CheckMobCount();
}
public void CheckMobCount()
{
mobCount = GameObject.FindGameObjectsWithTag("Enemy").Length;
print(mobCount);
}
The next piece of code is where the enemy is killed and the CheckMobCount() is called again.
public void TakeDamage()
{
enemyCurrentHealth -= 25;
enemyHealthBar.SetHealth(enemyCurrentHealth);
if (enemyCurrentHealth == 0)
{
Destroy(this.gameObject);
enemyList.CheckMobCount();
//needs death animations
}
}
Here's the console messages:
Console of printed lengths
I'm self taught so I apologize if this is elementary. I've tried doing this several different ways and this is the closest I've been but I'm open to new ideas as well.
Thank you!!
As noted in this answer, the object is not actually destroyed in the current frame.
From the documentation:
The object obj is destroyed immediately after the current Update loop… Actual object destruction is always delayed until after the current Update loop, but is always done before rendering.
I also agree that using DestroyImmediate() is a bad idea.
Ultimately, your question seems to really be about pausing the game when the enemy count reaches 0, which unfortunately hasn't actually been answered yet.
In fact, you don't really need to do anything different except move the check for the enemy count to the beginning of the Update() method, and pause the game there if it's 0. Then you'll find that the component for the enemy has been destroyed at that point.
Presumably enemies are spawned before the update loop starts (i.e. before the first frame), but if not then you can use whatever logic you're already using to decide that new enemies need to be spawned, to detect the fact that you haven't spawned any yet and avoid pausing before the enemies have spawned.
Here you have attached your script to your enemy instances. And they are still alive when you are querying for the number of enemies left.
You should do the following:
public class Enemy: MonoBehaviour
{
public static int EnemyCount = 0;
private void Start()
{
EnemyCount++;
}
private void OnDestroy()
{
EnemyCount--;
}
}
And then you can query the enemy count from anywhere but just excessing the EnemyCount by Enemy.EnemyCount.
If you want to get a more difficult example then you can check out this Game Dev tutorial: https://www.youtube.com/watch?v=LPBRLg4c5F8&t=134s
Destroy is actually executed at the end of the frame. There is DestroyImmediate that is executed immidiatelly but it's not recommended to be used. What I would do is to add a field or a property to identify whether the enemy is still alive and then to check against it. Something like:
class Enemy : MonoBehaviour
{
public bool IsAlive { get; set; } = true;
}
public class EnemyList : MonoBehaviour
{
//...
public void CheckMobCount()
{
mobCount = GameObject.FindGameObjectsWithTag("Enemy").Select(x => x.GetComponent<Enemy>()).Count(x => x.IsAlive);
print(mobCount);
}
}
And then:
public void TakeDamage()
{
enemyCurrentHealth -= 25;
enemyHealthBar.SetHealth(enemyCurrentHealth);
if (enemyCurrentHealth == 0)
{
Destroy(this.gameObject);
this.GetComponent<Enemy>().IsAlive = false;
enemyList.CheckMobCount();
//needs death animations
}
}
This can be further optimized to store the Enemy somewhere and not use GetComponent every time but you get the idea.
As already mentioned by others the issue is that Destroy is executed delayed.
Actual object destruction is always delayed until after the current Update loop, but is always done before rendering.
You could simply count only the GameObjects that are still alive, those for which the bool operator is true.
Does the object exist?
It will be false for objects destroyed in that same frame.
E.g. using Linq Count
using System.Linq;
....
mobCount = GameObject.FindGameObjectsWithTag("Enemy").Count(e => e);
which basically equals doing
mobCount = 0;
foreach(e in GameObject.FindGameObjectsWithTag("Enemy"))
{
if(e) mobCount++;
}
There is no need for an additional property or Component.
I am suggesting you to use “DestroyImmediate” instead of “Destroy”,Then look at the result.
I have a better idea, why not just use static variables when spawning enemies?
void Start()
{
for (int i = 0; i < spawningChildren.Count; i++)
{
GameObject newWeakMob = Instantiate(weakMobs[0],
spawningChildren[Random.Range(0, 4)]) as GameObject;
mobCount++;
}
}
Do not use Linq
Do not use DestroyImmediate (it will freeze and bug your game, probably)
Avoid FindGameObjectsWithTag in loops, only in initialization.
Track your enemies in an array or list
When you destroy an enemy, remove it's reference from the list
Use the list count/length to get the real actual number.
I am building a 3d virtual Auction house in Unity for my semester's project where multiple users can join the host server and interact with the action items in the game and increase their bids. The updated bid values should be synced across all the users. I am able to sync player's movements across the network by adding my "character controller" to the "player prefab" of the network manager. Users are also able to interact with the other game object to increase the item's bid locally. I am facing problems in syncing the updated bids of each auction item across the network for every client.
I am adding each auction item to the "registered spawnable prefabs" list of the network manager.
Registered Spawnable Prefabs
This is the error I am getting
Trying to send command for object without authority. DataSync.CmdIncreaseBidUI
UnityEngine.Debug:LogWarning(Object)
Mirror.NetworkBehaviour:SendCommandInternal(Type, String, NetworkWriter, Int32, Boolean) (at Assets/Mirror/Runtime/NetworkBehaviour.cs:185)
DataSync:CmdIncreaseBidUI(Int32)
DataSync:Update() (at Assets/Scripts/DataSync.cs:38)
This is the script that I placed on my auction item. I am using the text mash pro game object to show the current bid of the auction item.
Game Scene
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Mirror;
public class DataSync : NetworkBehaviour
{
[SyncVar]
public int num = 100;
public TMPro.TextMeshPro textObj;
void Start()
{
}
[Command]
public void CmdIncreaseBidUI(int num)
{
num += 100;
RpcIncreaseBidUI(num);
}
[ClientRpc]
public void RpcIncreaseBidUI(int num)
{
this.num = num;
GetComponent<TMPro.TextMeshProUGUI>().text = "Current Bid $" + num.ToString();
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown("space"))
{
CmdIncreaseBidUI(num);
}
}
}
As said by default only the Host/Server has the Network Authority over spawned objects.
Unless you spawn them with a certain client having the authority over the object (using Networking.NetworkServer.SpawnWithClientAuthority) but this also makes no sense if multiple clients shall be able to interact with the object as in your case.
And [SyncVar]
variables will have their values sychronized from the server to clients
.. and only in this direction!
Now there is the option to gain the authority over an object via Networking.NetworkIdentity.AssignClientAuthority so yes, you could send a [Command] to the Host/Server and tell it to assign the authority to you so you can call a [Command] on that object.
However, this has some huge flaws:
As this might change very often and quick you always have to make sure first that you currently have the authority and also the Host/Server has to make sure authority is assignable to you
You don't know when exactly the Host/Server is done assigning the authority so you would have to send a ClientRpc back to the client so he knows that now he can send a Command back to the host ... you see where this is going: Why not simply tell the Host/Server already with the very first [Command] what shall happen ;)
Instead you have to (should/ I would ^^) put your logic into your player script (or at least a script attached to the player object you have authority over) and rather do something like
using System.Linq;
// This script goes on your player object/prefab
public class DataSyncInteractor : NetworkBehaviour
{
// configure this interaction range via the Inspector in Unity units
[SerializeField] private float interactionRange = 1;
// Update is called once per frame
void Update()
{
//Disable this compoennt if this is not your local player
if (!isLocalPlayer)
{
enabled = false;
return;
}
if (Input.GetKeyDown("space"))
{
if(FindClosestDataSyncItemInRange(out var dataSync))
{
CmdIncreaseBid(dataSync.gameObject, gameObject);
}
}
}
private bool FindClosestDataSyncItemInRange(out DataSync closestDataSyncInRange)
{
// Find all existing DataSync items in the scene that are currently actuve and enabled
var allActiveAndEnabledDataSyncs = FindObjectsOfType<DataSync>();
// Use Linq Where to filter out only those that are in range
var dataSyncsInRange = allActiveAndEnabledDataSyncs.Where(d => Vector3.Distance(d.transform.position, transform.position) <= interactionRange);
// Use Linq OrderBy to order them by distance (ascending)
var dataSyncsOrderedByDistance = dataSyncsInRange.OrderBy(d => Vector3.Distance(d.transform.position, transform.position));
// Take the first item (the closest one) or null if the list is empty
closestDataSyncInRange = dataSyncsOrderedByDistance.FirstOrDefault();
// return true if an item was found (meaning closestDataSyncInRange != null)
return closestDataSyncInRange;
}
// As you can see the CMD is now on the player object so here you HAVE the authority
// You can pass in a reference to GameObject as long as this GameObject has a
// NetworkIdentity component attached!
[Command]
private void CmdIncreaseBid(GameObject dataSyncObject, GameObject biddingPlayer)
{
// This is happening on the host
// Just as a little extra from my side: I thought it would probably be interesting to store the
// last bidding player so when it comes to sell the item you know
// who was the last one to bid on it ;)
dataSyncObject.GetComponent<DataSync>().IncreaseBid(biddingPlayer);
}
}
and then change your DataSync a little
[RequireComponent(typeof(NetworkIdentity))]
public class DataSync : NetworkBehaviour
{
// This is automatically synced from HOST to CLIENT
// (and only in this direction)
// whenever it does the hook method will be executed on all clients
[SyncVar(hook = nameof(OnNumChanged))]
public int num = 100;
public TMPro.TextMeshPro textObj;
public GameObject LastBiddingPlayer;
void Start()
{
if(!textObj) textObj = GetComponent<TMPro.TextMeshProUGUI>();
}
// This method will only be called on the Host/Server
[Server]
public void IncreaseBid(GameObject biddingPlayer)
{
// increase the value -> will be synced to all clients
num += 100;
// store the last bidding player (probably enough to do this on the host)
LastBiddingPlayer = biddingPlayer;
// Since the hook is only called on the clients
// update the display also on the host
OnNumChanged(num);
}
private void OnNumChanged(int newNum)
{
textObj.text = "Current Bid $" + num.ToString();
}
}
I have game for multiple players where each user selects their hero before game starts and that loads the selected heroes into the battle arena.
I have small issue with getting the instantiation to spawn in correct numbers of players
The method that I have for Spawning the characters:
private void Placement()
{
for (int i = 0; i < SelectedCards.Count; i++)
{
for (int t = 0; t < AvailableHeroes.Count; t++)
{
if (AvailableHeroes[t].name == SelectedCards[i].name)
{
Debug.Log(AvailableHeroes[t]);
// Instantiate(AvailableHeroes[t], PlayerSpawnLocation[t].transform.position, transform.rotation);
}
{
}
}
}
}
This script checks for amount of selected hero cards and puts it against my list that has all the available heroes to choose from(prefabs).
The debug.log shows that only the correct heroes get called.
Instantiate ends up spawning a loot of heroes instead of the selected amount.
For clarity I attach full class:
{
private int playerSize; //amount of choices for card selection
private GameManager GM;
[Header("Lists for Spawning in Heroes")]
public List<GameObject> SelectedCards;
public List<GameObject> AvailableHeroes;
public List<Transform> PlayerSpawnLocation;
[Header("Canvas used for the game")]
public Transform GameCanvas;
public Transform CharacterCanvas;
//When scene starts it takes how many players will be picking a card.
void Start()
{
//connects this script with gamenmanager to be able to manipulate the cameras
GM = GameObject.Find("GameManager").GetComponent<GameManager>();
//gets playersize information from main menu selection
PlayerPrefs.GetInt("PlayerSize");
playerSize = PlayerPrefs.GetInt("PlayerSize");
SelectedCards = new List<GameObject>();
//enables/disables correct canvas not to cause any problems when we initiate this scene
GameCanvas.gameObject.SetActive(false);
CharacterCanvas.gameObject.SetActive(true);
}
// Update is called once per frame
void Update()
{
if (playerSize <= 0)
{
Placement();
GM.CharacterSelectionCamera.enabled = false;
GameCanvas.gameObject.SetActive(true);
CharacterCanvas.gameObject.SetActive(false);
GM.BattleCamera.enabled = true;
}
}
public void PlayerSelected(int cardPicked)
{
playerSize -= cardPicked;
}
private void Placement()
{
for (int i = 0; i < SelectedCards.Count; i++)
{
for (int t = 0; t < AvailableHeroes.Count; t++)
{
if (AvailableHeroes[t].name == SelectedCards[i].name)
{
Debug.Log(AvailableHeroes[t]);
// Instantiate(AvailableHeroes[t], PlayerSpawnLocation[t].transform.position, transform.rotation);
}
{
}
}
}
}
}
I hope someone can explain where I am going wrong with this.
Thanks,
I got the answer, I guess I was just being tired from working and could not see the obvious.
For those who wonder what solution is
The method gets called each frame thus it continues to endlessly spawn objects
There are 2 ways to fix it
1 Make coroutine and then return after you make your initial batch
2 Use a boolean at update so not only it checks player size but also whenever it can spawn it or not, you set the boolean to false after method get called.
I did not even notice the update function part.
Just a heads up, in your start function, PlayerPrefs.GetInt("PlayerSize"); is not doing anything since the value is not saved anywhere.
I'm trying to implement a damage over time system, but Unity keeps saying "Trying to Invoke method...Couldn't be Called." The method I want to call uses the parameters "Collider coll", but from my research you can't invoke if the method has said paremters.
Here is my code:
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;
public class DamageOverTime : MonoBehaviour
{
public int PHP; //PHP = Player Health from PlayerHealth.cs script.
public int Damage; //Amount of damage.
public int DamageOverTime; //Damage over time.
public float DamageInterval_DOT = .25f; //Damage interval for damage over time.
public string Level;
PlayerHealth player;
void Start()
{
player = GameObject.Find("Player").GetComponent<PlayerHealth>();
InvokeRepeating("OnTriggerEnter", DamageInterval_DOT, DamageInterval_DOT);
}
void Update()
{
PHP = GameObject.Find("Player").GetComponent<PlayerHealth>().PlayerHP;
if (PHP <= 0)
{
SceneManager.LoadScene(Level);
}
}
void OnTriggerEnter(Collider coll)
{
if (coll.gameObject.tag == "Player")
{
GameObject.Find("Player").GetComponent<PlayerHealth>().PlayerHP = PHP - Damage;
}
if (coll.gameObject.tag == "Ball")
{
gameObject.SetActive(false);
SceneManager.LoadScene(Level);
}
}
}
My goal is to get the OnTriggerEnter function to loop ever 1/4 of a second (or lower possibly). Current upon entering a collider my health is drained by 60% in about a second which is far too fast. How should I work around this?
You can't use InvokeRepeating with OnTriggerEnter, because it's a trigger, which means it will trigger once when entrance of its holder occured.
Also InvokeRepeating means that you want to keep repeating an action continously which is not the case here. You want your trigger to occur once and then remove health points over time.
Solution - Coroutine
Unity3D makes custom usage of IEnumerable and yield keyword called Coroutine that always returns an IEnumerator. How it works? It will return control on every yield there is in our Coroutine and then will go back to exact point where it gave back control instead of starting function execution from scratch.
Code:
void OnTriggerEnter(Collider coll)
{
if (coll.gameObject.tag == "Player")
{
StartCoroutine("DamageOverTimeCoroutine");
}
if (coll.gameObject.tag == "Ball")
{
gameObject.SetActive(false);
SceneManager.LoadScene(Level);
}
}
public IEnumerator DamageOverTimeCoroutine()
{
var dotHits = 0;
while (dotHits < 4)
{
//Will remove 1/4 of Damage per tick
GameObject.Find("Player").GetComponent<PlayerHealth>().PlayerHP -= Damage / 4;
dotHits++;
//Will return control over here
yield return new WaitForSeconds(DamageInterval_DOT);
//And then control is returned back here once 0.25s passes
}
}
There's of course room for improvement in this Coroutine. You can pass parameters to it, same as you can to any other method in C#. Also you can implement other invervals that are not hardcoded. Code above is just a simple example on how to deal with such scenarios.
For continous damage over time
public IEnumerator DamageOverTimeCoroutine()
{
var dotHits = 0;
var player = GameObject.Find("Player").GetComponent<PlayerHealth>();
while (true)
{
//Stop removing damage, player is dead already
if (player.PlayerHP <= 0)
yield break;
//Will remove 5 Damage per tick
player.PlayerHP -= 5;
dotHits++;
//Will return control over here
yield return new WaitForSeconds(DamageInterval_DOT);
//And then control is returned back here once 0.25s passes
}
}
To stop Coroutine somewhere else from code use StopCoroutine("DamageOverTimeCoroutine") to stop certain coroutine type or StopAllCoroutines() to stop all coroutines that are active now.
Here is the example of code in C#. What I am trying to do is to make the same in java. Overall, I have Enemy class which can create different types of enemies. Every enemy has its own behaviour.
// Creating list of behaviours for enemies
private List<IEnumerator<int>> behaviours = new List<IEnumerator<int>>();
// Adding behaviours into list
private void AddBehaviour(IEnumerable<int> behaviour)
{
behaviours.Add(behaviour.GetEnumerator());
}
public static Enemy CreateSeeker(Vector2 position)
{
var enemy = new Enemy(Art.Seeker, position);
enemy.AddBehaviour(enemy.FollowPlayer());
return enemy;
}
// Method of type IEnumerable<int> to be able to add it to List. This method contains behaviour
// of enemy.
IEnumerable<int> FollowPlayer(float acceleration = 1f)
{
while (true)
{
Velocity += (PlayerShip.Instance.Position - Position).ScaleTo(acceleration);
if (Velocity != Vector2.Zero)
Orientation = Velocity.ToAngle();
// yield keyword allows to return function and resume when it will be called one more time.
yield return 0;
}
}
I took this code from this guide: http://gamedevelopment.tutsplus.com/tutorials/make-a-neon-vector-shooter-in-xna-more-gameplay--gamedev-10103
And now I am trying to make the same implementation in my java code. But I met some problems.
I do not really understand how to add method FollowPlayer into a list. And even how should this method look like. Below I show my own implementation. But seems that my game goes into forever loop and everything pauses.
// We will be adding Iterable<Enemy> methods with enemy behaviour into our Iterable<Enemy> type
ArrayList private ArrayList<Iterator<Enemy>> behaviours = new ArrayList();
private void addBehaviour(Iterable<Enemy> behaviour) {
behaviours.add(behaviour.iterator());
}
public static Enemy createSeeker(Vector2 position){
Enemy enemy = new Enemy(AssetLoader.seeker, position);
enemy.addBehaviour(enemy.followPlayer());
return enemy;
}
private Iterable<Enemy> followPlayer() {
while (true) {
velocity.add(PlayerShip.instance.position.sub(position).scl(acceleration));
if (velocity != Vector2.Zero)
orientation = velocity.angle();
Thread.yield();
}
}
I just need help to understand that am I doing wrong and even am going in a right direction. Of course, I could create for every enemy type different class which will contain it behaviour and specifications.
Especially I am not sure about the setting Iterable as return type and how do I use Thread.yield() function.
I will be grateful for any kind of help. Thank you in advance!