Unity 2D project structure: How to create Player dynamically based on script - c#

I need a little help for my inheritance of my player in my little 2D Game.
In fact, I don't know how I can create a new player, and add it to a list.
This is my project structure :
As you can see, in my scripts, I have a GameBase script, and PlayerBase script with PlayerLife, PlayerController and PlayerAimManager as child of PlayerBase.
In my prefabs, I have my camera with GameBase script, and PlayerPrefab, with player scripts.
My PlayerBase is a script witch define a player, like his life, speed, color etc... :
public class PlayerBase : MonoBehaviour
{
public int Id { get; set; }
public string Name { get; set; }
protected float Life { get; set; }
public Color Color { get; set; }
protected WeaponBase PrimaryWeapon { get; set; }
protected WeaponBase SecondaryWeapon { get; set; }
protected WeaponBase SelectedWeapon { get; set; }
protected ScoreBase Score { get; set; }
protected bool IsDead { get; set; }
protected float Speed { get; set; }
protected float JumpForce { get; set; }
protected virtual void Start()
{
IsDead = false;
//Reset rotation
transform.rotation = Quaternion.identity;
Life = 100;
Speed = 20;
JumpForce = 4000;
PrimaryWeapon = gameObject.AddComponent<SMG>();
SecondaryWeapon = gameObject.AddComponent<Shotgun>();
SelectedWeapon = PrimaryWeapon;
}
}
(And my child works like this :)
public class PlayerLife : PlayerBase
{
// Use this for initialization
protected override void Start()
{
base.Start();
var color = base.Color();
}
}
Ok, now this is my problem :
How can I instantiate a new player and save it in list.
In my GameBase I do something like this:
public class GameBase : MonoBehaviour
{
public List<PlayerBase> Players { get; set; }
void Start()
{
var playerObj1 = Instantiate(Resources.Load("Prefabs/Players/PlayerPrefab")) as GameObject;
PlayerBase player1 = playerObj1.GetComponent<PlayerBase>();
player1.Color = Color.red;
player1.Name = "Naografix Red";
var playerObj2 = Instantiate(Resources.Load("Prefabs/Players/PlayerPrefab")) as GameObject;
PlayerBase player2 = playerObj2.GetComponent<PlayerBase>();
player2.Color = Color.blue;
player2.Name = "Foo Blue";
Players = new List<PlayerBase>{
player1,
player2
}
}
}
If I do this, my color in my child player scripts don't have my color because is not static. But if I put Color to static prop, I can't change my color in my GameBase.
Well, I don't know if my inheritance is good or not.
I just want to create a player dynamically with child script based on PlayerBase to get his own variable.
EDIT
In fact, I don't know how I can create dynamically a player with custom properties.
I want to do this :
PlayerBase player1 = new PlayerBase();
player1.Name = "Nao";
player.Color = Color.Red;
And store it in my PlayerList. But I can't do this, because, no prefab is created.
So I instantiate a new GameObject with my PlayerPrefab.
-Ok, cool, my player is spawned, I'm going to apply some properties. Huh? How can I get my PlayerBase script?
Then, I get my PlayerBase with GetComponent, but still not working because I got my child (I don't have PlayerBase on my PlayerPrefab).
And if I put PlayerBase on it, every child have a different instance of my PlayerBase.
To conclude : How can I create a new Player based on PlayerBase, share all properties to my childs and spawn a PlayerPrefab with their properties ?
ENDEDIT
EDIT With simple example
public class Game : MonoBehaviour
{
private List<PlayerBase> _players;
public GameObject Player;
void Start () {
_players = new List<PlayerBase>();
var obj = Instantiate(Player);
var playerBase = obj.GetComponent<PlayerBase>();
playerBase.Color = Color.blue;
}
void Update () {
}
}
public class PlayerBase : MonoBehaviour {
public Color Color { get; set; }
// Use this for initialization
protected virtual void Start () {
gameObject.GetComponent<Renderer>().material.SetColor("_Color", Color);
}
// Update is called once per frame
protected virtual void Update () {
}
}
public class PlayerLife : PlayerBase {
// Use this for initialization
protected override void Start () {
gameObject.GetComponent<Renderer>().material.SetColor("_Color", Color);
}
// Update is called once per frame
void Update () {
}
}
My playerLife color is null. This is my first problem.
ENDEDIT
Ask me if you want more details.
Thanks

When you define a public member (not a property) in a MonoBehaviour, the value set by the inspector is automatically serialized for that game object. So that when you instantiate from that game object it will be automatically restored into the instantiated game object.
This also can be done without public accessor and with [SerializeField] attribute.
Make sure that only your PlayerLife script inherits from PlayerBase. The rest of your scripts(PlayerController and PlayerAimManager) should inherit from MonoBehaviour.
As, for your color problem, it is not showing because when you do player2.Color = Color.blue;, you modifying a struct which is a copy not a reference. You have to manually assign that color to gameObject.GetComponent<Renderer>().material.SetColor.
Your new PlayerBase script:
public class PlayerBase : MonoBehaviour
{
public Color Color;
public Color matColor
{
get
{
Color = gameObject.GetComponent<Renderer>().material.GetColor("_Color");
return Color;
}
set
{
Color = value;
gameObject.GetComponent<Renderer>().material.SetColor("_Color", Color);
}
}
// Use this for initialization
protected virtual void Start()
{
matColor = Color;
}
// Update is called once per frame
protected virtual void Update()
{
}
}
PlayerLife:
public class PlayerLife : PlayerBase
{
public PlayerBase pBase;
void Awake()
{
pBase = this;
}
// Use this for initialization
protected override void Start()
{
matColor = Color;
}
// Update is called once per frame
protected override void Update()
{
}
}
To use it from your game scriot:
public class Game : MonoBehaviour
{
private List<PlayerLife> _players;
public GameObject Player;
void Start()
{
_players = new List<PlayerLife>();
GameObject obj = Instantiate(Player);
PlayerLife playerLife = obj.GetComponent<PlayerLife>();
playerLife.matColor = Color.blue;
_players.Add(playerLife);
GameObject obj2 = Instantiate(Player) as GameObject;
PlayerLife playerLife2 = obj2.GetComponent<PlayerLife>();
playerLife2.Color = Color.red;
_players.Add(playerLife2);
}
void Update()
{
}
}
Now, if you decide to access PlayerBase from your PlayerController and PlayerAimManager script,
PlayerBase playerBase = gameObject.GetComponent<PlayerBase>();
OR
PlayerBase playerBase = gameObject.GetComponent<PlayerLife>().pBase;

Related

Is there a ways for cinemachine to retarget player after it is destroyed and instantiated as a clone?

I am working on a 2D platformer and I am using cinemachine to follow my player.
When a player drops off a platform under -20 y, the player is destroyed and instantiated as a clone to the spawn point as expected, but the camera is not following, as the original player is destroyed: it says missing on the "Follow" Slot.
Is there any way to solve it? I prefer using Destroy and Instantiate as respawning instead of teleporting the original player to the respawn point.
This is the respawn script (GameMaster)
public class GameMaster : MonoBehaviour
{
public static GameMaster gm;
void Start()
{
if (gm == null)
{
gm = GameObject.FindGameObjectWithTag("GM").GetComponent<GameMaster>();
}
}
public Transform playerPrefab, spawnPoint;
public int spawnDelay = 2;
public void RespawnPlayer() {
//yield return new WaitForSeconds(spawnDelay);
Instantiate(playerPrefab, spawnPoint.position, spawnPoint.rotation);
Debug.Log("ADD SPAWN PARITCAL");
}
public static void Killplayer(Player player) {
Destroy(player.gameObject);
gm.RespawnPlayer();
}
}
here is the player script if it is needed
public class Player : MonoBehaviour
{
[System.Serializable]
public class PlayerStats
{
public int Health = 100;
}
public PlayerStats playerStats = new PlayerStats();
public int FallBoundary = -20;
void FixedUpdate()
{
if (transform.position.y <= FallBoundary)
{
DamagePlayer(1000);
}
}
public void DamagePlayer(int damage) {
playerStats.Health -= damage;
if (playerStats.Health<=0)
{
Debug.Log("Kill Player");
GameMaster.Killplayer(this);
}
}
}
You can cache the cinemachine inside your start method and then assign to follow the player at respawn.
Your code will become
using Cinemachine;
public class GameMaster : MonoBehaviour
{
public static GameMaster gm;
public CinemachineVirtualCamera myCinemachine;
void Start()
{
if (gm == null)
{
gm = GameObject.FindGameObjectWithTag("GM").GetComponent<GameMaster>();
}
myCinemachine = GetComponent<CinemachineVirtualCamera>();
}
public Transform playerPrefab, spawnPoint;
public int spawnDelay = 2;
public void RespawnPlayer() {
//yield return new WaitForSeconds(spawnDelay);
var newPlayer = Instantiate(playerPrefab, spawnPoint.position, spawnPoint.rotation);
Debug.Log("ADD SPAWN PARITCAL");
myCinemachine.m_Follow = newPlayer;
}
public static void Killplayer(Player player) {
Destroy(player.gameObject);
gm.RespawnPlayer();
}
}
You must assign the new object to follow like this:
myCinemachine.m_Follow = spawnedPlayer;

Creating an Enemy Class

I've never done this before and my true understanding of classes are not that good. However, I plan on mastering it after this project! What I'd like to do is create a class to determine enemy type by TAG: Enemy1 or Boss. (I've already designed a system to randomize the Enemy1 stats so no two will be the same. However, here, I just want to learn how to properly setup enemies stats so here's my code)
using System.Collections;
public class Enemies : MonoBehaviour {
public float MaxHp;
public static float Hp;
GameObject enemy = GameObject.Find("Enemy1");
GameObject boss = GameObject.Find("Boss");
void Awake()
{
AssignStats(enemy, MaxHp);
}
public static void AssignStats (GameObject en, float MaxHp)
{
if (en.tag == "Enemy1")
{
MaxHp = 50;
Hp = MaxHp;
Debug.Log(Hp);
}
if (en.tag == "Boss")
{
MaxHp = 500;
Hp = MaxHp;
Debug.Log(Hp);
}
}
}
This code doesn't seem to work. Why?
Enemy Class : Enemy.cs (not Monobehavior)
using UnityEngine;
[System.Serializable]
public class Enemy
{
public EnemyType EnemyType;
public GameObject EnemyPrefab;
public string EnemyTag;
public int MaxHealth;
public int EnemyDamage;
public Vector3 SpawnPos;
private int _currentHealth;
public void Init()
{
_currentHealth = MaxHealth;
}
public void UpdateHealth(int newHealthValue)
{
_currentHealth = newHealthValue;
}
public void ReceiveDamage(int damage)
{
var updatedHealth = _currentHealth - damage;
UpdateHealth(updatedHealth > 0 ? updatedHealth : 0);
}
}
Enemies Class : Enemies.cs that manage all enemies, randomize between enemies
using UnityEngine;
public enum EnemyType
{
Enemy1,
Enemy2,
Enemy3,
Enemy4,
Enemy5,
Boss
}
public class Enemies : MonoBehaviour
{
public Enemy[] AllEnemies;
//Initial Value
public int NumberOfEnemies = 3;
private void Start()
{
InitEnemies(NumberOfEnemies);
}
public void InitEnemies(int howManyEnemies)
{
for(int i= 0; i < howManyEnemies; i++)
{
var randomIndex = Random.Range(0, AllEnemies.Length - 1);
SpawnEnemy(AllEnemies[randomIndex]);
}
}
public void SpawnEnemy(Enemy enemy)
{
Instantiate(enemy.EnemyPrefab, enemy.SpawnPos, Quaternion.identity);
enemy.Init();
}
}
You can see that I assigned all enemies data in the inspector that come up from the array of Enemy in enemies class, it has Enemy Prefab, position, damage etc.
If you have any question, feel free to ask :)
Cheers!
if I understand corrlcy.
you don't need to pass parameters to AssignStats method because the all you need property in the class.
I would use gameObject.tag to get the current append object tag.
if you append to Enemy1 component you will do gameObject.tag == "Enemy1" condition.
if you append to Boss component you will do gameObject.tag == "Boss" condition.
you just append the script to your role component and tag right tag.
using System.Collections;
public class Enemies : MonoBehaviour {
public float MaxHp;
public float Hp;
void Awake()
{
AssignStats();
}
public void AssignStats ()
{
if (gameObject.tag == "Enemy1")
{
MaxHp = 50;
Hp = MaxHp;
Debug.Log(Hp);
}
if (gameObject.tag== "Boss")
{
MaxHp = 500;
Hp = MaxHp;
Debug.Log(Hp);
}
}
}
I would do it like that :
//enum contains all your enemies
public enum EnemyType
{
Enemy1,
Boss
}
public class Enemies : MonoBehaviour
{
//This will be assigned in the inspector
public EnemyType CurrentEnemyType;
//You don't need them to be public since you are hardcoding them.
private float MaxHp;
private float Hp;
void Awake()
{
AssignStats();
}
public void AssignStats()
{
if (gameObject.CompareTag(CurrentEnemyType.ToString()))
{
if (CurrentEnemyType == EnemyType.Enemy1)
{
MaxHp = 50;
Hp = MaxHp;
Debug.Log(Hp);
}
// instead of doing separated if blocks, you need to do if else for less code execution
else if (CurrentEnemyType == EnemyType.Boss)
{
MaxHp = 500;
Hp = MaxHp;
Debug.Log(Hp);
}
/*
More simplified way instead of the if else, if you assume that all your enemies except the boss have 50 hp.
MaxHp = CurrentEnemyType == EnemyType.Boss ? 500 : 50;
Hp = MaxHp;
Debug.Log(Hp);
*/
}
}
}
Cheers!

Item/Ability system using scriptableobjects in Unity

I'm new to Unity and I'm having some trouble creating scriptable objects for items to pick up and use in my project. I've created the initial set-up to make the items with variables that I can plug-and-play with.
What I'm trying to do is:
1 - plug in a monobehavior script for each new item's behavior when used.
2 - Allow the player to pick up the item on collision and use the item on key command. The player should only carry one item at a time.
Here is what I have so far.
the scriptable object (itemObject.cs)
[CreateAssetMenu(fileName = "New Item", menuName = "Item")]
public abstract class Item : ScriptableObject {
public new string name;
public string description;
public int damageAmount;
public int affectDelay;
public int affectDuration;
public int affectRange;
public float dropChance;
public float itemLife;
}
I have a script that will trigger if the player collides with the object (itemDisplay.cs)
Currently, this doesn't do anything but destroys the item.
public class itemDisplay : MonoBehaviour {
public Item item;
public static bool isActive = false;
void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "Player")
{
var playerPickUp = other.gameObject;
var playerScript = playerPickUp.GetComponent<Player>();
var playerItem = playerScript.playerItem;
playerScript.pickUpItem();
bool isActive = true;
Object.Destroy(gameObject);
}
}
void Start () {
}
void Update(){
}
}
Currently, I have a pickup function and a use item function that's being handled by my player script(Player.cs). Currently, this just toggles a boolean saying that it did something when the item is collided with. My big question is how/where should I create and reference the item ability and how do I pass it to the player script here?
public void pickUpItem()
{
playerItem = true;
//itemInHand = itemInHand;
Debug.Log("You Picked Up An Item!");
// Debug.Log(playerItem);
if(playerItem == true){
Debug.Log("This is an item in your hands!");
}
}
public void useItem()
{
//use item and set
//playerItem to false
playerItem = false;
}
If your question is "how do I create an instance of an Item in my project", you do that by right clicking in your Project view, then selecting Create -> Item. Then drag the Item asset that you created from your Project view into the ItemDisplay scene object's item field in the Inspector.
Regarding how to pick up the item, you would pass the item to your Player script from your ItemDisplay script. You've got most of that already wired up.
public class ItemDisplay : MonoBehaviour
{
void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "Player")
{
var playerPickUp = other.gameObject;
var playerScript = playerPickUp.GetComponent<Player>();
var playerItem = playerScript.playerItem;
// This is where you would pass the item to the Player script
playerScript.pickUpItem(this.item);
bool isActive = true;
Object.Destroy(gameObject);
}
}
}
Then in your Player script...
public class Player : MonoBehaviour
{
public void PickUpItem(Item item)
{
this.playerItem = true;
this.itemInHand = item;
Debug.Log(string.Format("You picked up a {0}.", item.name));
}
public void UseItem()
{
if (this.itemInHand != null)
{
Debug.Log(string.Format("You used a {0}.", this.itemInHand.name));
}
}
}
You should also make your Item class non-abstract, or leave it abstract and create the appropriate non-abstract derived classes (Weapon, Potion, etc.). I put an example of that below.
You can't attach a MonoBehaviour to a ScriptableObject if that's what you're trying to do. A ScriptableObject is basically just a data/method container that isn't tied to a scene. A MonoBehaviour has to live on an object within a scene. What you could do however, is add a "use" method to your Item class, and call that from a MonoBehaviour within the scene when the player uses an item.
public abstract class Item : ScriptableObject
{
// Properties that are common for all types of items
public int weight;
public Sprite sprite;
public virtual void UseItem(Player player)
{
throw new NotImplementedException();
}
}
[CreateAssetMenu(menuName = "Items/Create Potion")]
public class Potion : Item
{
public int healingPower;
public int manaPower;
public override void UseItem(Player player)
{
Debug.Log(string.Format("You drank a {0}.", this.name));
player.health += this.healingPower;
player.mana += this.manaPower;
}
}
[CreateAssetMenu(menuName = "Items/Create Summon Orb")]
public class SummonOrb : Item
{
public GameObject summonedCreaturePrefab;
public override void UseItem(Player player)
{
Debug.Log(string.Format("You summoned a {0}", this.summonedCreaturePrefab.name));
Instantiate(this.summonedCreaturePrefab);
}
}
Then change your UseItem method in Player:
public class Player : MonoBehaviour
{
public void UseItem()
{
if (this.itemInHand != null)
{
this.itemInHand.UseItem(this);
}
}
}

Disable instantiate and button click

There is issue that i facing with two objects and one button. One is cube second is ground when we click on button cube is collide with ground destroy and instantiate again. On Cube collision score is decrement.Also in hierarchy there is Empty game object which name is controller which has method of text score.Score is working fine but i want that when score is 0 then button click does not work and cube is not instantiate.
Cube :
Ground :
Controller :
CubeScript:
public class Cube : MonoBehaviour {
Rigidbody2D body;
void Start () {
body = GetComponent<Rigidbody2D>();
body.isKinematic = true;
}
}
Ground Script:
public class Ground : MonoBehaviour {
private Button button;
private BoxCollider2D collide;
public GameObject object1Clone;
void Start () {
collide = GetComponent<BoxCollider2D>();
collide.isTrigger = true;
button = GameObject.FindGameObjectWithTag ("Button").GetComponent<Button> ();
button.onClick.AddListener (() => Magnetic ());
}
void OnTriggerEnter2D(Collider2D target) {
Destroy (target.gameObject);
Instantiate (object1Clone, new Vector3 (0f, 4.12f, 0f), Quaternion.identity);
}
public void Magnetic(){
GameObject.FindGameObjectWithTag ("Player").GetComponent<Rigidbody2D> ().isKinematic = false;
}
}
ScoreScript:
public class ScoreScript : MonoBehaviour {
public static int Score=1;
void OnTriggerEnter2D(Collider2D target) {
if (Score <=0) {
} else {
Score--;
Controller.instance.SetScore(Score);
}
}
}
Controller:
public class Controller : MonoBehaviour {
public static Controller instance;
public Text scoreText;
void Start () {
scoreText.text = ""+1;
if(instance==null){
instance=this;
}
}
public void SetScore(int score){
scoreText.text =""+score;
}
}
First change the listener registration to this:
button.onClick.AddListener (Magnetic);
this will make it easier to remove the listener.
I will show you two ways of doing it, an easy one and a proper one a bit harder to grasp. So if you don't quite get it, use the first and learn about the second.
Every time you decrease the score, check for it and call for the appropriate action:
public class ScoreScript : MonoBehaviour {
public static int Score=1;
void OnTriggerEnter2D(Collider2D target)
{
Score--;
Controller.instance.SetScore(Score);
if(Score <= 0){
GameObject.Find("ground").GetComponent<Ground>().ClearButtonListener();
}
}
}
And in the Ground component:
public void ClearButtonListener()
{
button.onClick.RemoveListener (Magnetic);
}
Now the second more appropriate way would be to use event and listener
public class ScoreScript : MonoBehaviour, IScoreHandler {
public static int Score=1;
public event Action OnScoreZero = () => {};
void OnTriggerEnter2D(Collider2D target)
{
Score--;
Controller.instance.SetScore(Score);
if(Score <= 0){
OnScoreZero();
}
}
}
public interface IScoreHandler{ event Action OnScoreZero; }
And your listeners listens.
public class Ground : MonoBehaviour {
private Button button;
private BoxCollider2D collide;
public GameObject object1Clone;
private IScoreHandler scoreHandler = null;
void Start () {
scoreHandler = GameObject.Find("Score").GetComponent<IScoreHandler>();
if(scoreHandler != null){
scoreHandler.OnScoreZero += ClearButtonListener;
}
collide = GetComponent<BoxCollider2D>();
collide.isTrigger = true;
button = GameObject.FindGameObjectWithTag ("Button").GetComponent<Button> ();
button.onClick.AddListener (Magnetic);
}
void OnDestroy(){
if(scoreHandler != null){
scoreHandler.OnScoreZero -= ClearButtonListener;
}
}
}
Thanks to interface and event, your class is no more relying on another class but on an interface which makes it more flexible and scalable.
You need to set the field interactable of the UnityEngine.UI.Button object to false, see http://docs.unity3d.com/ScriptReference/UI.Button.html, i.e. use
void OnTriggerEnter2D(Collider2D target) {
if (Score <=0) {
/* disable the button */
GameObject.FindGameObjectWithTag ("Button").GetComponent<Button>().interactable = false;
}
in your ScoreScript.cs.

Use of addComponent instead of new keyword in unity

I have an interface which all my monsters should implement.
namespace Assets.Scripts
{
interface IMonster
{
void setSpeed(float s);
float getSpeed();
void SetMonsterPosition(Vector2 pos);
Vector2 GetMonsterPosition();
void DestroyMonster();
void MoveMonster();
}
}
Then I have a concrete monster class (I will add more):
public class Monster2 : MonoBehaviour, IMonster
{
public Monster2()
{
speed = Random.Range(0.05f, 0.15f);
monster = (GameObject)Instantiate(Resources.Load("Monster2"));
float height = Random.Range(0, Screen.height);
Vector2 MonsterStartingPosition = new Vector2(Screen.width, height);
MonsterStartingPosition = Camera.main.ScreenToWorldPoint(MonsterStartingPosition);
monster.transform.position = MonsterStartingPosition;
}
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
And a factory class that will produce my monsters:
class MonsterFactory : MonoBehaviour
{
public static IMonster getMonster()
{
return new Monster2();
}
}
This works but I read that I should not use new and I should use AddComponent. So I tried something like:
class MonsterFactory : MonoBehaviour
{
public static GameObject mymonster; //#first
public static IMonster getMonster()
{
return mymonster.AddComponent<Monster2>(); //#second
}
}
The problem is that now when I am trying to run the game there is a an error NullReferenceException: Object reference not set to an instance of an object
IMonster monster = MonsterFactory.getMonster();
As David said mymonster needs to be initialised. But even then you will run into trouble, as there is one GameObject containing a bunch of Monster2 components.
So instead I suggest:
GameObject go = new GameObject (GameObjectName);
return go.AddComponent<Monster2>();
Now every new monster has its own GameObject and thus can move indepently.

Categories