How to use polymorphism in anemic domain model design? - c#

Now i am working on a mmorpg server,Let's talk one scene that player drop something from inventory to world
If I design the Drop with rich domain model,i will create code like this
class Player {
void Drop(IDropable dropable,vector3 pos){
RemoveFromInventory();
dropable.BeDroped(pos);
Net.Broadcast(pos);
}
}
As you can see,the dropable will be called BeDroped(pos),and each entity which implement IDropable will do something special,for example,a weapon be drop,it will hit somebody;a magic ball be dropped,the player's heath point will decrease...
but if I want to design by anemic domain model,how could I use polymorphism for each IDropable?here is the example,the way i can achieve is using if else or switch ,but I know it is stupid.
class DropService {
void RemoveFromPlayerInventory(Player player){
//.....
}
void Drop(Player player, IDropable dropable,vector3 pos){
RemoveFromPlayerInventory(player);
if(dropable is Weapon)
OnDropWeapon(dropable,pos);
else if(dropable is magicball)
OnDropMagicBall(dropable,pos);
//.....
Net.Broadcast(pos);
}
}

One way to get rid of those if/else chains: double dispatch
Example:
class DropService:IDropService {
void RemoveFromPlayerInventory(Player player){
//.....
}
void Drop(Player player, IDropable dropable,vector3 pos){
RemoveFromPlayerInventory(player);
dropable.BeDropped(this, pos);
//.....
Net.Broadcast(pos);
}
void IDropService.Drop(Weapon item, Vector3 pos){ /*impl.*/ }
void IDropService.Drop(Magicball item, Vector3 pos){ /*impl.*/ }
}
public interface IDropable
{
void Bedropped(IDropService dropper, Vector3 pos);
}
public interface IDropService
{
void Drop(Weapon item, Vector3 pos);
void Drop(Magicball item, Vector3 pos);
}
public class Weapon: IDropable
{
public override void BeDropped(IDropService dropper, Vector3 pos)
{
dropper.Drop(this, pos); // Will automagically call the right overload
}
}
// Same for Magicball
As you see, there is no more if/else or switch.
Downside: What people consider to be a code smell is that DropService now needs to know every implementation of IDropable. So anytime a new class implements IDropable, you also need to change DropService.
To avoid this, you could consider a variety of possible patterns. Like several Factory patterns (Factory that creates an appropriate DropBehavior for example?), (Drop-)Strategy pattern ...

One approach could be to treat game objects more as database-entries than actual .net types. So you could for example create an game object in an editor, assign it some graphics, and one or more effects that should happen on some event. So your object might look something like
private EffectManager effectManager;
private Dictionary<EventId, EffectId> effectsOnEvents= new ();
public void OnEvent(EventId eventId) {
if(effectsOnEvents.TryGet(eventId, out var effectId){
effectManager(this, effectId);
}
}
So a weapon would have the DropWeapon effect attached to the Drop event, a magic ball would have the DropMagicBall effect attached and so on. Some class would need to define what each of these effects actually does, but there might be much fewer effects than game objects. Now your player would just need to call item.OnEvent(EventId.Drop), and that would trigger any associated behaviors.
You could also for example add support for multiple scripts to allow things like a magic ball weapon that both damages and heals. And add parameters to the script, so you can easily change the amount of damage dealt or hitpoints restored by updating a value in a database.
I would highly recommend Eric Lippers articles on Wizards and warriors where he points out that using the type system to model domain behaviors can be problematic.

Related

How to send an event/message to an unrelated game object in Unity/C#?

Let's say I have two entities, a Player and an Enemy. Each of them would have its own C# script. On the Enemy, it has this basic health manager code:
[SerializeField] float health = 3;
public void TakeDamage(float damage)
{
health -= damage;
if (health <= 0) {
Destroy(gameObject);
// SOME EVENT HERE?
}
}
What I want is for the Player to know he's killed the Enemy (as well as knowing which enemy he destroyed). However, I'm not sure how to do this. I've looked at a few approaches:
Actions/delegates; they would require the Player to 'import' a public action/delegate from the Enemy (if I understand them correctly; I'm still new to C#), and I don't want a dependency between these two things... they're conceptually unrelated; I shouldn't have to 'import' from Enemy on the Player.
Using Unity events (like the ones you configure in UI, for example button OnClick()); this won't work either, because both Player and Enemy might be instantiated at runtime, not pre-defined.
In my head I'm imagining the Player script would have something like this listening to events:
void OnEnemyDestroyed(GameObject enemy) { ...do things in reaction to enemy death here... }
Is this possible?
For this specific situation my advice is using interface. For example:
public interface IUnit
{
void OnEnemyKilled(IUnit enemy);
}
Both player and enemy script will implement this interface, the TakeDamage method also need to append a new parameter with the type IUnit.
class Enemy : IUnit
{
public void TakeDamage(float damage, IUnit attacker = null)
{
health -= damage;
if (health <= 0) {
attacker?.OnEnemyKilled(this);
Destroy(gameObject);
}
}
public void OnEnemyKilled(IUnit enemy){}
}
This approach doesn't only solve the problem, the advantage is at some point, a single damage value is not enough, you may need information about who makes this attack, then you can add additional methods to the interface and execute them in TakeDamage.
Note that you'd better prepose the callback, so that in it you can keep accessing all the properties of the killed entity.
To answer the question in comment, there are many approaches to make sense, you can:
Pass null TakeDamage(10f); (Above method is updated)
Pass a default IUnit implementation
class NotAUnit : IUnit
{
public static readonly NotAUnit Instance = new();
public void OnEnemyKilled(IUnit enemy){}
}
TakeDamage(10f, NotAUnit.Instance);
An overloaded method. (As same as approach 1).
If it is about in general have a global event for listening on the destroying of objects you could have an additional component like
public class DestroyEvent : MonoBehaviour
{
public static event Action<GameObject> OnDestroyed;
private void OnDestroy()
{
OnDestroyed?.Invoke(gameObject);
}
}
So you could just globally attach a listener to
DestroyEvent.OnDestroyed += Listener;
...
private void Listener(GameObject destroyedObject)
{
Debug.Log(destroyedObject);
// e.g. list all attached components
foreach(var component in destroyedObject.GetComponents<Component>())
{
Debug.Log(component.GetType());
}
}
So as soon as an object has that component attached the moment it is destroyed you will receive the callback, regardless of whether it is spawned on runtime or even the component attached afterwards on runtime.
Have in mind that this is just a simple example - you will also get the event currently in case the scene is changed or you exit the app. But you can build on top of this and e.g. globally turn the event on and of with an additional flag etc

Trying to change music between scenes

I'm trying to store the current music that is playing in the current scene that I'm on and then in the next scene change it for another one.
Here are my scripts.
AudioManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AudioManager : MonoBehaviour {
public static AudioManager Instance;
private MusicList musicList;
[SerializeField] private AudioSource _effectsSource, currentMusic;
private void Awake() {
if (Instance == null) {
Instance = this;
DontDestroyOnLoad(this);
}
else Destroy(this);
Instance = this;
musicList = GetComponentInChildren<MusicList>();
}
public void PlayMusic(MusicId id) {
AudioSource musicToPlay = musicList.GetMusicSource(id);
musicToPlay.Play();
currentMusic = musicToPlay;
}
public void ChangeMusic(MusicId newMusicId) {
currentMusic.Stop();
AudioSource musicToPlay = musicList.GetMusicSource(newMusicId);
musicToPlay.Play();
currentMusic = musicToPlay;
}
public void PlaySound(AudioClip clip) {
_effectsSource.PlayOneShot(clip, 0.1f);
}
}
MusicList is a children of the AudioManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MusicList : MonoBehaviour
{
public Music[] musicList;
private void Awake()
{
foreach (Music music in musicList) {
music.source = gameObject.AddComponent<AudioSource>();
music.source.clip = music.clip;
music.source.volume = music.volume;
music.source.loop = true;
}
}
public AudioSource GetMusicSource(MusicId id) {
foreach (Music music in musicList) {
if (music.id == id) return music.source;
}
return null;
}
}
Music
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class Music
{
public MusicId id;
public AudioClip clip;
[HideInInspector]
public AudioSource source;
public float volume;
}
public enum MusicId {
Menu,
Gameplay
}
When I debug in the first scene the current track playing is stored, but when I change the scene and try to access the currentMusic value it's null.
Thank you for the help!
You have at least two approaches here. One of them is using proprietary scene data (not a technical term), meaning you use whatever info the current scene has, while the other method is using data persistence (this is a technical term) to "save" data in (normally) transient objects after scene changes. There is a third way to persist data, but it's not recommended for your use-case.
There are use-cases for all of them, each having different aims. Let's attack them:
scene info
This is straight forward: you have a menu, a tutorial, 2 levels, a boss fight level then the "end game + credits". This would mean 6 different songs (assuming each scene, including the end-credits has a different theme). Once you switch from menu to level and from level to level and then to "the end", a new song pops-up on the audio manager.
The simplest approach here is to have a GameObject in each scene (preferably with the same name and same components, only the song being different). Let's call it "SceneInfo". On this GO you can attach any info necessary to your level, like some description of the level, objectives, difficulty or song. You can use the Find() function to locate this GO then just access the component you need (like a component that saved the Music, then just pop-it on the AudioSource and Play() it).
Minor note: the scene GO must NOT be set as DontDestroyOnLoad(). Because you must not "carry" it in a different scene since each scene has its own stuff. And, of course, this should be different from your AudioManager GO.
This is prolly what you want for your approach.
data persistence
This is normally used to "carry" player-related stuff like the player icon, player name, health, inventory etc. from an earlier scene to this one. You can "carry" the music too, I guess, assuming it's a custom playlist OR if you want to continue the song. Otherwise, I don't recommend this for music if the songs are always different between levels.
This is basically what you did with your AudioManager.
storage (not recommended for what you need)
There would be a third option related to storage, by using PlayerPrefs, but I think this is overkill just for a song. But feel free to use this if you find it easier. As I said, it all depends on your use-case.
This option refers mostly to game preferences like volume, graphics settings etc. It's basically the "memory" of game settings.
Minor caveats:
I would set currentMusic; as public.
Your Awake() function has a weird flow. else Destroy(this); is missing a return. It's not wise to set the Instance to this since you're destroying the object.
Try this instead:
private void Awake()
{
if (Instance == null)
{
DontDestroyOnLoad(this);
Instance = this;
}
else if (Instance != this)
{
Destroy(this);
return;
}
if (musicList == null)
musicList = GetComponentInChildren<MusicList>();
}
You said: MusicList is a children of the AudioManager. I think you mean component instead of children, if the gameObject it's attached to is called "AudioManager", right? Else it doesn't quite make sense. A child class is something else.
Then the code in PlayMusic() is identical to ChangeMusic() (apart from stopping the previous song to update it). That means if you want to change something in the code for PlayMusic() or ChangeMusic(), you'll always have to do it twice (in each function). This is better:
public void PlayMusic(MusicId id) {
AudioSource musicToPlay = musicList.GetMusicSource(id);
musicToPlay.Play();
currentMusic = musicToPlay;
}
public void ChangeMusic(MusicId newMusicId) {
currentMusic.Stop();
PlayMusic(newMusicId);
}
... which can be simplified even more to this:
public void PlayMusic(MusicId id) {
currentMusic = musicList.GetMusicSource(id);
currentMusic.Play();
}
public void ChangeMusic(MusicId newMusicId) {
currentMusic.Stop();
PlayMusic(newMusicId);
}
And now there is no temp variable. It complicated the code for no reason.
(of course, assuming the id is always guaranteed to be in the list, which, of course, it should be). This way you only have one place to change your code or fix potential bugs.
While you can "force" serialization of C# objects, it's better to use Unity objects (anything from UnityEngine.object, but mostly rely on ScriptableObjects, Components and Prefabs).
The music init workflow is also weird. Instead of initializing the AudioSource on each music wrapper, you do it in a loop in the list. It's a bit "backwards", but not necessarily a bad thing. I just wouldn't do it.

Variable not being set in class using inheritance

Edit: It turns out it was how I was getting the ProjectileWeapon component. I was getting the one that was on the non-instantiated prefab instead of getting the one on weapon gameobject. I changed it so that the code instantiates the game object first (or gets the existing one if we've already picked it up), and then get the component from that. So the rest of the code works fine. Now I can move on and improve it!
I have an issue with the class below called ProjectileWeapon. It is based on an abstract class called Weapon, and that class inherits MonoBehaviour.
Weapon has two abstract functions called BeginCycle and EndCycle which are implemented in the ProjectileWeapon class. Those functions set a variable called "firing".
The problem is, "firing" doesn't ever seem to be set despite the functions being called correctly. I know the functions are called because I can see the prints in the console.
Also, when I use that variable in the update function, it doesn't do anything because the variable never changes.
The OnGUI function is working and is displaying text on screen, however the "firing" variable is never updated.
Am I misunderstanding how to use inheritance?
This class is on the weapon prefab which is then instantiated during the equipping function in the game
public class ProjectileWeapon : Weapon
{
private bool firing;
private float firingTimer;
void Start()
{
print("ProjectileWeapon start");
}
void OnGUI()
{
GUI.Label(new Rect(0,100,100,100), "ProjectileWeapon firing: " + firing);
}
void Update()
{
// this function is called but "firing" is not updated
}
public override void BeginCycle()
{
print("projectile begin cycle");
firing = true;
}
public override void EndCycle()
{
print("projectile end cycle");
firing = false;
}
}
Here's the base class:
public abstract class Weapon : MonoBehaviour
{
public abstract void BeginCycle();
public abstract void EndCycle();
}
EDIT: Here is the code that calls the above
This component is added to the player game object
public class WeaponHandler : MonoBehaviour
{
public bool FireInput { get; set; } // set to true when user holds the mouse button down, and false when let go
public Weapon WeaponBehaviour; // this is the script that does the weapon functionalility. Any subclass of Weapon can be put here e.g. ProjectileWeapon, MeleeWeapon
private bool isFiring = false;
void OnGUI()
{
GUI.Label(new Rect(0,0,100,100), "fire input:" + FireInput + ", isFiring:" + isFiring);
}
void Update()
{
// fire weapon
if (WeaponBehaviour && FireInput && !isFiring)
{
ActivateWeapon();
}
else if (!FireInput && isFiring)
{
DeActivateWeapon();
}
}
private void ActivateWeapon()
{
print("activate weapon");
isFiring = true;
WeaponBehaviour.BeginCycle();
}
private void DeActivateWeapon()
{
print("deactivate weapon");
isFiring = false;
WeaponBehaviour.EndCycle();
}
}
Based on the code you've provided, there are three possible scenarios:
You're not calling the functions. However, if the print functions are called, then you must be calling them.
You're calling both functions, which sets the variable to true, and then to false.
You're overriding the variable with a local variable with the same name. Visual Studio will warn you if that's the case.
It's hard to tell without knowing where the variables are supposed to be called. If you upload the rest of the code, I'm sure the answer will be clear.
It turns out it was how I was getting the ProjectileWeapon component. I was getting the one that was on the non-instantiated prefab instead of getting the one on weapon gameobject. I changed it so that the code instantiates the game object first (or gets the existing one if we've already picked it up), and then get the component from that. So the rest of the code works fine. Now I can move on and improve it!

How to tell which of the colliders has been collided?

I'm creating a game in which there are enemies, I want to have headshots in the game so I have 2 colliders: one to the head and one to the body. I can't find any good way to tell which is which in the code.
I thought of a solution but I don't like it- a different type of collider to the head, and different type to the body (like polygon and box colliders). It works but I don't think it's good enough (if I want to add more colliders or have two of the same type that wouldn't work).
virtual protected void OnTriggerEnter2D(Collider2D collider2D)
{
if (collider2D.gameObject.tag.Equals("Zombie"))
{
Destroy(gameObject);//destroy bullet
Zombie zombie = collider2D.gameObject.GetComponent<Zombie>();
if (collider2D is BoxCollider2D)
zombie.HeadShot(demage);//headshot
else zombie.BulletHit(demage);//normal hit
}
}
I want a way to tag the colliders somehow so I can tell between them.
You need to create public variables of type BoxCollider2D and assign your colliders. When a collision occurs call an IF statement inside of OnTriggerEnter to see which one has collided. This will work no matter if there are more of the same types of collider.
public class Example : MonoBehaviour
{
public BoxCollider2D box01;
public BoxCollider2D box02;
private void OnTriggerEnter2D(Collider2D collision)
{
if(collision.IsTouching(box01))
{
Debug.Log("1");
}
else if(collision.IsTouching(box02))
{
Debug.Log("2");
}
}
}
isTouching is a Unity method which returns a bool depending on a collider that is comparing.
I would suggest to not add all colliders on the same GameObject but rather give each collider it's own child GameObject (this way you also can see easily which colliders belongs to which outline in the scene view ;) )
Then you could use a class with an enum to define which type of collider you have there:
public class BodyPart : MonoBehaviour
{
public BodyPartType Type;
}
public enum BodyPartType
{
Head,
LeftArm,
RightArm,
Body,
LeftLeg,
RightLeg
}
and attach it to all body parts next to each collider.
Then you could do something like
virtual protected void OnTriggerEnter2D(Collider2D collider2D)
{
if (collider2D.gameObject.tag.Equals("Zombie"))
{
Destroy(gameObject);//destroy bullet
// Note you then should use GetComponentInParent here
// since your colliders are now on child objects
Zombie zombie = collider2D.gameObject.GetComponentInParent<Zombie>();
var bodyPart = collider2D.GetComponent<BodyPart>();
switch(bodyPart.Type)
{
case BodyPartType.Head:
zombie.HeadShot(demage);//headshot
break;
// you now could differ between more types here
default:
zombie.BulletHit(demage);//normal hit
break;
}
}
}

How to refactor my code to avoid raising events from outside the classes?

I am developing a game which contains some managers for different tasks such as collision detection, etc. Once a collision is detected, each gameobject affected by this collision should raise the OnCollided event, so I can easily play sounds, make the object disappear, open a door or whatever, but I cannot get this behavior without raising the event from the CollisionSystem, instead of letting each GameObject raise their own event.
Here is a simplified example, hope it makes sense:
Class CollisionSystem
{
// It is called when a collision has been detected between two objects (code not included)
public void HandleCollision(GameObject gameObject1, GameObject gameObject2)
{
//Do whatever
//How can trigger each corresponding game object event from here?
}
}
Class GameObject
{
protected event GameEventHandler Collided;
protected void OnCollided(GameEventArgs e)
{
if (Collided != null)
Collided(this, e);
}
}
I’ve tested things like making the OnCollided method public, but the event should be raised from inside the class... And, on the other hand, the game object itself cannot determine when it collides, cause, well, this is what the CollisionSystem does.
Thanks in advance.
Alright this is what I could come up with that should embody what you're trying to do:
public class CollisionSystem
{
Public void HandleCollision(IGameObject[] objects)
{
foreach (IGameObject obj in objects)
{
//call the function that tells the object it collided
obj.CollisionDetected()
}
}
}
You'll want your gameobjects to implement an interface
public interface IGameObject
{
public void CollisionDetected()
}
Which means your gameobject-class looks like this;
public class GameObject : IGameObject
{
//constructors, params, whatever
public void CollisionDetected()
{
//play some sound, remove object, whatever
}
}
The thing is; I don't know what raises your collision-event.
Edit: so I reread your question and saw you mentioned it's begin called from the physics engine. Wherever/however that may be, if you have your CollisionSystem declared inside the physics-engine class, you can call the HandleCollision()-method from there and you'd be full circle.

Categories

Resources