3

I am relatively new to class design and I have a task that I'm not sure how best to complete, or whether my idea in general is a code smell.

I'm developing a RPG where people can own monsters, so each one is an object instance of the Monster class. Since the sprite of a monster changes depending on a few different factors, the Monster class has a method for drawing the sprite that I call which does all the checks and logic and displays the correct sprite using the object's properties and I just call it like:

$Monster->drawSprite();

This method does checks using other class methods, such as

$this->isHoldingItem();

which checks data from the database using a unique monster ID (a class property) and stored as object properties.

Now and then for the story and in other places, I want to draw a monster sprite that isn't actually owned by anyone, so has no database row or object instance. Rather than hardcode the image URL or duplicate my code, is there any way a method can be called both as part of an instance and statically and respond accordingly? Is this even a good idea?

So in my head I could do:

 $Monster->drawSprite();    

This will use the instance properties to determine all the necessary logic to display the sprite, or I could do:

Monster::drawSprite();

To draw a monster sprite without relying on class properties. Maybe I could pass an array as an argument for all the necessary object property checks the method usually relies on.

I imagine I could do something using func_get_args() or lots of conditionals but I could see that turning into spaghetti code quickly. Is this a good idea, or is it even possible? I'm specifically referring to PHP with this question.

  • What if the monsters belonged to the game? That way you could save them to the DB and treat them like normal monsters, the only difference would be the owner of the monster. – npinti Jan 20 '15 at 15:48
  • @npinti I could do this but it does not seem necessary when I simply want to display the sprite and adding them to the game would involve adding a lot of superfluous extra data and DB checks. – Roy Jan 20 '15 at 15:55
3

This isn't an answer to your question, but a solution to your problem.

The issue here is that $Monster->drawSprite() is a non-starter; having monsters know how to draw themselves is a violation of the Single Responsibility Principle. For that same reason it doesn't make sense to try to make a method pull "double duty" by being both an instance method and static method, since you don't want the instance method at all.

You should separate the game logic from I/O. More specifically, you should be able to run the game loop without any video/audio/input devices (and I don't mean providing no-op mock interfaces). A good solution would look something like this:

  • The Monster class only contains data relevant to the game logic.
  • There exists a mapping from monster types (and other game entity types) to their corresponding sprites (and sound effects, etc.) This may involve adding a new field to your game objects.
  • There exists a function to go from game world coordinates to screen coordinates. (This is again a matter of separation of concerns. Separating them has the benefit that your game logic won't be affected by graphical resolution.)
  • There exists a drawSprite function that takes a sprite and screen coordinates and just draws it.

During normal gameplay you iterate over all game entities, find the correct sprite, calculate its screen coordinates, figure out if it's even visible, and pass all the info to drawSprite. During scripted events you just call drawSprite directly.

This might seem like extra work but you'll be thankful you did it as your game grows. Keeping IO and logic separate means you can reason about them more easily, test them separately, makes it impossible for changes in the graphics code to break your game logic, and reduces the scope you need to look at when a bug creeps in.

  • I appreciate your response to my answer but I'm afraid when I say RPG I may have mislead you into believing it's much more complicated than what it really is. When I say RPG I mean more like Neopets...no I/O, no game world, no co-ordinates. I am literally just printing an image on a web page, all I need for it is to get the correct image. – Roy Jan 21 '15 at 12:06
  • 2
    @Roy: No I/O means that the "game" doesn't communicate with the outside world in any way. Displaying sprites is a form of I/O. – Bart van Ingen Schenau Jan 21 '15 at 15:18
  • @BartvanIngenSchenau My apologies for the misunderstanding. What I am trying to say is my 'game' is nothing more than some static text and images with dynamic data pulled from a database like stats, monsters etc... – Roy Jan 22 '15 at 10:53
2

If a method uses $this it cannot be called statically. Or at least it shouldn't be called statically even if some sort of weird PHP behavior allows it. You never know with PHP.

If you insist on keeping your architecture as is, i.e. with monsters drawing themselves (which is not a good architecture, like Doval said), the simplest way would be something like this.

Monster class has a static method drawSprite($img_url, $maybe_other_params) which accepts all data needed to draw a monster as it's arguments. You can pass it i.e. image url. It also has a method drawSelf() which draws itself, like your current drawSprite() does, but by calling drawSprite() and passing all the required parameters.

$usersMonster->drawSelf(); //called non statically
Monster::drawSprite($img_url, $other_stuff); // called statically

class Monster
{
    public function drawSelf()
    {
        $other_stuff = someComplicatedLogic();
        self::drawSprite($this->img_url, $other_stuff);
    }
    public static function drawSprite($img_url, $other_stuff)
    {
         // do the sprite drawing stuff
    }
}

Although I should stress again it's not a good architecture. Doval's suggestion is what you really need. Even if it's not a complicated app... especially if it's not a complicated app: start developing good habits early, before you move to complicated stuff.

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.