Class design

Hi,

I have just started coding with MonoGame and to get started I have set myself a project to build a simple shooter; a player, player bullets, enemy and enemy bullets – that simple.

My code design ‘so-far’, is a game class, a player class, and an enemy class. In the game class update method I call my player and enemy update methods, same for the draw method (in the game class draw method).

My questions are with bullets. Is it a good design to create a separate bullet class to manage bullets from the player and enemy with it’s own draw and update methods? Would these be called in the player update / draw methods or in the game class update draw methods?

What is the best practice for this?

Just looking for some good advice.

Thank you.

A good class design would be to find common things of all your instances and subclass everything accordingly.

Do you have any benefit of separating the bullets to player and enemy? In games the most common problem is actually performance. Instancing new bullets all the time is a bad idea and therefore you may want to have some object pooling or space partioning in place sooner or later - once fired they are unrelated to the player anyway so there is no reason to have them at the player class

1 Like

As @reiti.net wrote, good class design is based on finding commonalities between the objects you are trying to represent. In other words it’s about abstraction.

You are trying to create a class for bullets. What are bullets? They are objects that will travel in space according to a given trajectory. They will have a velocity, and possibly a model or texture that will represent them graphically. The above abstract definition will be true for both the player’s and the enemies’ bullets.

Some things may differ between the type of bullets that the player and the enemies shoot. The player bullets may be faster, may travel along a different trajectory, and may have a different texture or model. What you can do to account for these differences is implement a single Bullet class that will accept the velocity, trajectory and model/texture e.g. as parameters in the constructor. This way you can instantiate the objects of the same Bullet class, but have a different outcome depending on whether they belong to the player or the enemies.

Hope this helps :full_moon_with_face:

Thank-you.

Everything make good sense what you mention, but I am a little unsure on the pooling.

From what I can figure, allocate memory for say 100 bullets in an array, and have the bullets instanced, but not doing anything; perhaps a property in the bullet class ‘active = false’. When the bullet is required, set it’s XY, set the property ‘active’ to true, in the game class update and draw methods, if ‘bullet.active’ than do stuff with it. When the bullet has finished it’s task, set ‘active’ to false – this avoids the instancing and will pool the bullets…?

Is this ‘sort-of’ what you meant?

Thanks again.

Yes, that’s basically it. There are several ways of doing it but your’s should work totally fine

Thank you for confirming. I’ll have a play around with different ways of implementing this.

Thanks again.

Is it a good design to create a separate bullet class to manage bullets
from the player and enemy with it’s own draw and update methods?

Yes

Would these be called in the player update / draw methods or in the game
class update draw methods?

Preferably a game bullet list or a collision class that can accept them which has its own update / draw methods and can then call on the items interface or guarenteed virtual variables properties or methods to both move and then check them for collisions.

It’s good to have a player create a bullet with a reference that points back to its creating object. Then hand that off to some thing dedicated to tracking it and players for collisions.

1 Like

Just to add an alternate opinion on object pooling, I don’t do it at all in my games and I have never had performance issues related to them so I would recommend keeping the code as simple as possible and only optimizing based on performance profile data from your game. You can always add object pools later for things that need a performance boost.

Personally I would have a bullet class with it’s own update and draw BUT the important part is to reduce code duplication and complexity so if you have the same bullet drawing code in the player and enemy maybe figure out a way of just having it in one place.

As an alternative view, i strongly recommend against inheritance for reusing common data or behavior. Prefer composition and inject. I just created an auto-recycling physics based projectile system. It has a pool of recycled entities representing projectiles. They are designed using composition, no inheritance, which makes testing quite a bit easier.

Interesting topic.

I tend to favor abstract base classes or interfaces for items that have commonality’s are seperate and have to interact with each other or be acted on with common calling methods. Were their instances might even be passed to other classes or lists or arrays and worked on in a single chain.

This is just my typical goto choice for decoupling things with complex relationships so they are more maluable.

for example
you have bullets and players.
players kind of create bullets.
bullets and players have positions and velocitys.
bullets and players are not the same type of object but have similar or even share the same variables.
bullets and players will need to interact in collisions with each other and themselves.
friendly fire might be off.
It would be good to be able to have them both in a collision checking class were that is its responsibility.
This should have a single container that calls these generic methods in any type passed to it or can access the required variables.

Here is were you start to run into consequences in what choices you have made previously.
Because the tricky part is the containers that use them not the either type itself the containing classes that use them such as collision class or pooling lists. Which are in technical terms desirable to have the property of being “Aggregations” abstractions and interfaces give these types with those containing classes the property of being abstractions with extendable concrete implementations.

Imo
For some things you however do want them to have tightly coupled relationships and be dependent or have an “is a” relationship sometime you want clarity and direct to the point relationships, but…
Not for things like bullets and players. I typically want them to have a “has a abstract” relationship or interface or maybe even virtual methods.
So that the calls to these instances methods are generic and the implementations can be specific.

That’s sort of the way monogame itself works you have generic methods that work on specific operating systems or graphics api’s which have their own implementations.

but that is just how i look at it.
I think pooshoes advice is pretty solid if its simple just write it simply, however if you can’t see how to do it before you start then its probably not simple and then you should consider prototyping the barebone steps to it.

Humm i made a example to illustrate it may have made it a bit to big and typed it lazily with too little thought but.

Edited it to make it slightly cooler.

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System.Collections.Generic;

namespace AbstractItemsAndContainers
{
    /// <summary>
    /// This is the main type for your game.
    /// </summary>
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        Collisions collisions = new Collisions();

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = Globals.Instantiate(GraphicsDevice);

            // TODO: use this.Content to load your game content here

            // meh
            var specialPlayer = new Player(new Vector2(250, 250), new Vector2(.01f, .01f), new Vector2(50, 50), Color.Black);
            collisions.AddItem(specialPlayer);

            // Make some random players and bullets

            Random rnd = new Random(234234);
            for (int i =0; i < 100; i++)
            {
                var x = rnd.Next(10, 800);
                var y = rnd.Next(10, 800);
                var pos = new Vector2(x, y);
                var vel = Vector2.Normalize(new Vector2(250, 250) - pos);
                var p = new Player(pos, vel, new Vector2(20, 20), Color.Green);
                collisions.AddItem(p);
                if(i < 25)
                {
                    x = rnd.Next(10, 800);
                    y = rnd.Next(10, 800);
                    pos = new Vector2(x, y);
                    vel = Vector2.Normalize(new Vector2(250, 250) - pos) *5f;
                    var b = new Bullet(pos, vel, new Vector2(5, 5));
                    collisions.AddItem(b);
                }
            }
            
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// game-specific content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // TODO: Add your update logic here
            collisions.Update(gameTime);

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here
            spriteBatch.Begin();
            collisions.Draw(gameTime);
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }


    // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    // @@@@@@@@@@@@@@@@@@        Example classes       @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

    public static class Globals
    {
        public static SpriteBatch spriteBatch;
        public static GraphicsDevice gd;
        public static Texture2D dotTexture;
        public static SpriteBatch Instantiate(GraphicsDevice device)
        {
            gd = device;
            dotTexture = TextureDotCreate(device, Color.White);
            spriteBatch = new SpriteBatch(gd);
            return spriteBatch;
        }
        public static Texture2D TextureDotCreate(GraphicsDevice device, Color c)
        {
            Color[] data = new Color[1];
            data[0] = c;

            return TextureFromColorArray(device, data, 1, 1);
        }
        public static Texture2D TextureFromColorArray(GraphicsDevice device, Color[] data, int width, int height)
        {
            Texture2D tex = new Texture2D(device, width, height);
            tex.SetData<Color>(data);
            return tex;
        }
    }
    public abstract class Iamhereigothere
    {
        public Vector2 position;
        public Vector2 velocity;
        public float speed;
        public Vector2 size = new Vector2(50,50);
        public Color color = Color.White;

        public virtual void Update(GameTime gameTime)
        {
            position += velocity;
        }
        public virtual void Draw(GameTime gameTime)
        {
            var rect = new Rectangle(position.ToPoint(), size.ToPoint());
            // should pass this but this is a dependancy anyway you look at it.
            Globals.spriteBatch.Draw(Globals.dotTexture, rect, Color.Red);
        }
    }

    public class Player : Iamhereigothere
    {
        public Player(Vector2 startingPosition, Vector2 startingVelocity, Vector2 startingSize, Color col)
        {
            position = startingPosition;
            velocity = startingVelocity;
            speed = Vector2.Distance(startingVelocity, Vector2.Zero);
            size = startingSize;
            color = col;
        }
        public Bullet Shoot()
        {
            return new Bullet(position, velocity *3f, new Vector2(10, 10));
        }
        // we can overidde default behavior.
        public override void Draw(GameTime gameTime)
        {
            var rect = new Rectangle(position.ToPoint(), size.ToPoint());
            // should pass this but this is a dependancy anyway you look at it.
            Globals.spriteBatch.Draw(Globals.dotTexture, rect, color);
        }
    }
    public class Bullet : Iamhereigothere
    {
        public Bullet(Vector2 startingPosition, Vector2 startingVelocity, Vector2 startingSize)
        {
            position = startingPosition;
            velocity = startingVelocity;
            speed = Vector2.Distance(startingVelocity, Vector2.Zero);
            size = startingSize;
        }
    }

    public class Collisions
    {
        public List<Iamhereigothere> items = new List<Iamhereigothere>();

        public void AddItem(Iamhereigothere item)
        {
            items.Add(item);
        }
        public void RemoveItem(Iamhereigothere item)
        {
            items.Remove(item);
        }

        public void Update(GameTime gameTime)
        {
            BruteForceCheckCollision(gameTime);
            foreach (var item in items)
            {
                item.Update(gameTime);
            }
        }
        public void BruteForceCheckCollision(GameTime gameTime)
        {
            for (int i = 0; i < items.Count; i++)
            {
                var item = items[i];
                for (int j = 0; j < items.Count; j++)
                {
                    if (i != j)
                    {
                        var testedItem = items[j];
                        if (TestCollision(item, testedItem))
                        {
                            // collision 
                            // increase color
                            var n = items[i].color.R + 50;
                            if (n > 255)
                                n = 255;
                            items[i].color.R = (byte)n;
                            n = items[j].color.R + 50;
                            if (n > 255)
                                n = 255;
                            items[j].color.R = (byte)n;
                            // change direction
                            var dif = items[i].position - items[j].position;
                            if (dif.X == 0 && dif.Y == 0)
                            {
                                // this just prevents nans
                            }
                            else
                            {
                                var unitLengthDirection = Vector2.Normalize(items[i].position - items[j].position);
                                items[i].velocity = unitLengthDirection * items[i].speed;
                                items[j].velocity = -unitLengthDirection * items[j].speed;
                            }
                        }
                    }
                    else
                    {
                        // no collision 
                        // reduce color
                        var n = items[i].color.R - 1;
                        if (n < 0)
                            n = 0;
                        items[i].color.R = (byte)n;
                        n = items[j].color.R - 1;
                        if (n < 0)
                            n = 0;
                        items[j].color.R = (byte)n;
                    }
                }
            }
        }
        public bool TestCollision(Iamhereigothere a, Iamhereigothere b)
        {
            var recta = new Rectangle(a.position.ToPoint(), a.size.ToPoint());
            var rectb = new Rectangle(b.position.ToPoint(), b.size.ToPoint());
            if (recta.Intersects(rectb) || recta.Contains(rectb))
                return true;
            else
                return false;
        }
        public void Draw(GameTime gameTime)
        {
            foreach(var item in items)
            {
                item.Draw(gameTime);
            }
        }
    }
}

Is it a good design to create a separate bullet class to manage bullets from the player and enemy with it’s own draw and update methods?

What do you mean by seperate bullet class? Do you mean defining a new class which is responsible for managing bullets? Are you thinking something like a factory pattern? If so, than yes I could see some advantages to decoupling your code this way.

My advice though is just write code to get your end-goal ideas working for your game. Don’t worry about performance. You can come back later after to clean it up if necessary. Over time with more developer experience you can start writing more performant code the first time aroujnd.

Yes willmotil, your description is pretty good. In general, “has-a” is better than “is-a” :wink:

“Is it a good design to create a separate bullet class to manage bullets from the player and enemy with it’s own draw and update methods?”

I suggest that you encapsulate bullet state and changes that involve only that state in a bullet, and likewise for a player. I suggest you use an event system to communicate actions between them. For example, ‘FireBullet’ could be an event that the player sends to a bullet event dispatcher, which uses a factory to create a bullet.

If you use dependency injection quite heavily, you can always add functionality, like object pooling, with no significant impact on your system.

If that doesn’t make sense, start simple. The advice to code with a goal is good. Just do this:

  1. Do the simplest thing that works.
  2. Do the simplest thing that works for the next thing.
  3. Extract the duplication.
  4. Rinse and repeat.

I would always advocate for test-driving that development because testing will constrain your design so it cannot be over-coupled. But start simple. :slight_smile: