4
\$\begingroup\$

I'm starting game development to learn a new language (SDL2, C++) and would like some opinions on the best way to handle a probably very basic problem.

I don't want my player to go off screen, so I have something like this now:

void Player::sanitizePosition() {
    if ( m_xpos < 0 ) {
        m_xpos = 0;
    }

    if ( m_ypos - m_height < 0 ) {
        m_ypos = m_height;
    }

    ...
}

For the other two sides I would need the screen dimensions. Now I'm wondering what the best way to give them to the object would be?

Global variables are bad, but a Singleton with the screen information seems like the same thing to me.

Another idea would be to have a GameObject base class, that has the screen dimensions as static members, which Player would inherit from.

Of course I could put them into the constructor of every game object, but that just screams code smell.

This is such a basic problem, I'm sure there is a consensus on how to do it.

\$\endgroup\$
6
  • \$\begingroup\$ If you need it for every object on screen, there is something wrong with your overall design. I also don't find find the dependency injection suggested by William any better. I´d say only the class resolving collisions really need to know the screen dimensions(and of course the code that created the window). \$\endgroup\$
    – wondra
    Aug 26 '15 at 10:05
  • \$\begingroup\$ @wondra My Player class is resolving the collision, or did I not understand you correctly? Do you suggest not letting the players reposition themselves and let an outside class handle that? \$\endgroup\$
    – Minix
    Aug 26 '15 at 10:27
  • \$\begingroup\$ "Opinions" is a dangerous word on this site. \$\endgroup\$
    – Ocelot
    Aug 26 '15 at 10:43
  • \$\begingroup\$ @Minix yes, physics engine usually resolves collisions. Having player resolve its collisions violates OOP "single responsibility principle" anyway. Though you probably don't have actual physics, you can just give the fancy name to some "collision resolver". Keeping relevant code and/or data together might prove helpful in long run for maintainability. \$\endgroup\$
    – wondra
    Aug 26 '15 at 11:15
  • 1
    \$\begingroup\$ @Ocelot I live on the edge, maaaaan. \$\endgroup\$
    – Minix
    Aug 26 '15 at 11:16
1
\$\begingroup\$

I guess there are many feasible design approaches. For me what has worked best is that I created two classes: GameCamera and GameInit. GameInit holds all possible resolutions and is fairly technical, but passes the essential stuff, such as the best available screen resolution, monitor ratio etc. to the one and only instance of GameCamera.

Now, you may now (perhaps rightly) say: "Are you kidding me?? GameCamera, a singleton?! Nope, I dare to say there are actually few things where a singletons can be useful, and that is if you know absolutely sure that you will not need a second instance of the class and you provide an additional 'initializer' and 'updater' functions for your game objects that will collect the essential global data from the singleton(s) (and where inheritance would be rather cumbersome to implement as you just don't want your bullet to be also camera or mouse...!

To make things work, you gotta be careful though. I use in all my other classes two essential functions: Simplified that's InitAllGlobalData() which is called only once after all classes have been initialized, and UpdateData() which is naturally placed in the GameLoop.

InitAllGlobalData() instantiates all the static global data for your object. In this function, you will instantiate your member data by copying all the essential stuff from GameCamera, e.g. {myScreenSize=Ref.ScreenSize} etc. The advantage is that, (1) if you break my rule and dare to create a second instance of GameCamera, you could exceptionally help yourself by calling again the function or its overloaded instance InitAllGlobalData(GameCamera &Ref) where you only update the new camera stuff, without worrying that somewhere in line 14291 in your collision algorithm of your 12th .cpp file there is the need to find again the edge of the universe; (2) all access to this singleton and other global data is clean and tidy in that one function and thus easy to find/modify if you ever revisit your code.

The other, bool UpdateData() copies all member data that is dynamic, such as, Zoom, Scroll etc. They get updated in the Gameloop, although GameCamera may return prematurely if there is nothing to update. Hence, your collision objects that bounce around access dynamic members of GameCamera through a pointer call in UpdateData() and update their own member data through that.

For example, GameCamera holds stuff like Zoom(), ScrollEdge() etc., so the bullet's object own private member myZoom is updated when needed within UpdateData() {MyZoom=GameCamera->GetCurrentZoomLevel();). Note that there is no other call to GameCamera anywhere in my class, except by the power of those two functions--tame your singletons!

However, I felt this is also not an ideal design when dealing with different resolutions on different computers when we have to deal with static "HUD" (head-up-design) objects like radars, message screen, messages etc.

I thus added secondary screen-percentage units as a member function of GameCamera (going from 0 to 100) for positioning of HUDs. For example, I want a radar always to be on the top right corner (=90,90) and no matter the resolution of the screen. Now since the screen is probably more wide than high, x units are automatically adjusted by this ratio, but not the radar size (10 units) as it would appear stretched. Naturally, in my DisplayScreen class, I have the aforementioned InitAllGlobalData() function that initalizes through that the necessary members, but no UpdateData(), as I do not need anything dynamic like Scroll, Zoom etc. from GameCamera.

This ensured me that I have the same design and same game feel, no matter the prevailing screen resolution etc. I have faced no issues so far in terms game design (currently 35k lines code) and can reuse my code in other projects, as I just refurbish my UpdateData() and InitAllGlobalData() functions. But, to be honest with you, I have not plunged into networking yet, and Update() may need some additional thoughts when coming to synchronizing at runtime, but I will update this post once I get to that stage.

\$\endgroup\$
0
\$\begingroup\$

This is the approach I usually go with.

C# Example

class Scene
{
    public int Width;
    public int Height;
    public List<GameObject> Objects;

    /* TODO: Initialize the collection. */

    public void AddObject(GameObject gameObject)
    {
        this.Objects.Add(gameObject);
        gameObject.Scene = this;
    }

    public void RemoveObject(GameObject gameObject)
    {
        this.Objects.Remove(gameObject);
        gameObject.Scene = null;
    }
}

abstract class GameObject
{
    public Scene Scene;
}

This is a good OOP approach and allows you to have multiple Scenes with different dimensions should that ever be needed. It also allows you to do generic sanitizing in the Scene itself rather than having to implement it on each GameObject.

\$\endgroup\$
2
  • \$\begingroup\$ I literally have a scene graph on my whiteboard, because a friend asked me how he should do something similar yesterday. Talk about not seeing the forest for the trees. Will take that as a vote for a base class for now, since I want to be as incremental as possible. Thanks for your answer. \$\endgroup\$
    – Minix
    Aug 26 '15 at 9:58
  • \$\begingroup\$ Reminds me of the time I Googled a problem, found a great answer here and then found out I had posted said answer myself. As @wondra points out, this isn't necessarily the best/right solution, but it is a solution that works and is versatile. And usually that is all you need. \$\endgroup\$ Aug 26 '15 at 10:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.