Saturday, June 11, 2011

XNA Game Design: Implementing SCENE flow

Sorry I'm not updating as frequently. It's mostly because writing these things out is long, and I'm busy myself. Still, this chapter should conclude Part 1 of the guide!

By now, you might have noticed everything is coming back to the Game1 class. Because everything must actually run from this class, our code in it will certainly get a bit bogged down, and keeping our code organized will be more difficult.

Games, no matter how simple, should not be built entirely within the "Main" class (which is essentially what our Game1 is). It's not only impractical, but it makes for code that's both hard to read and keep organized. Not to mention, it makes us loose sight of what we need to focus on.


My solution to this may not be the best, but I'd say it's pretty darn good. If you remember from the chapter "The Basics", I showed you the simple game loop. While this is the loop that all games follow, it's not the loop an XNA game will follow.


And I'm not mentioning our Input!


Our XNA loop is a bit more complicated. And as we've already done, we've thrown our call to update our inputs in the Update() method. That works just fine.


Still, if we don't do something about organizing our code, we will wind up with a huge Game1 class. While there are a number of ways of engineering around this problem, I can't guarantee the solution I'll provide is the best (truthfully, the best solution depends on the game YOU'RE designing. What I'm showing you is best for something with lots of screens or minigames, such as a RPG). Still, I think it's pretty good, as it keeps the code organized and it allows the developer to focus on the situation at hand.


The solution we will use to overcome this obstacle is to create an interface that will handle our Update and Draw methods depending on what we want our situation to focus on. As such, our game flow will look more like this;


The flow may seem slightly more complicated, but because we have static input, all we'll have to focus on from here on out is our Draw() and Update() methods.

I call this interface a SCENE. The concept is that a different object that implements the SCENE interface will take over the update and draw methods depending on the situation, and in turn that will let us code objectively to each SCENario (get it? huh? Err, let me better explain...)



When you start a game, you are often confronted with a few splash screens and maybe a intro movie before encountering a title screen. From the title screen, you can start a new game, go to the options menu, or to the load menu. From the load menu and selecting 'new game', you go to the world map. From the world map, you can go to the main menu, to many submenus, or to a battle screen. Occasionally you might have a minigame, or something else. When you're coding each of these scenarios and screens, you should only be concerned with that scene, and any transitioning to any related screen. What happened on the main menu should stay on the main menu, and so forth.


All images Courtesy of Nintendo and Gamefreak. This game serves as a good example of how the SCENE interface should work. The scope of a game such as pokemon may be daunting, but when you break it down into it's individual components it's not as complex as it seems.

Each of these would be a different SCENE object. The SCENE Interface needs to only contain the method signatures of two methods; Update and Draw .
Alright, lets do this! Create a new interface object (similar to how you would create a new class, but select 'interface' instead of class). Name is IScene.cs (good habit; prefix all interfaces you create with a capital 'I'), and add the code;
public interface IScene
{
    void Update();
    void Draw(SpriteBatch s_batch);
}


Wow. Smallest .cs file ever or what?


While this whole process is all fairly painless, but we're not finished yet. We still have to make things work in our Game1 class. By the time this is over, it should be one of the last things we will ever do in the Game1 class. In the Game1 properties, add 
public static ISCENE SCENE;

Now in our Update, add:
//Check to see if Game1.SCENE = null. 
//If so, exit. Otherwise, update.

if (SCENE == null)
    this.Exit();
else SCENE.Update();


And in our Draw method, add (between our spriteBatch.Begin() and spriteBatch.End() calls) :
SCENE.Draw(spriteBatch);

And we're done! Build, fix any errors and run. What happens?
If the screens pops up and closes itself, it's working. Why? Because of our check in update. We never set the Game1.SCENE to anything, so it's value is null. When Game1.Update() runs, it trips the if statement, and it exits with this.Exit() (alas, another non-static member hosted in Microsoft.Xna.Framework.Game). This is good for two reasons;

Firstly, if somehow our code runs in a manner to nuke the current SCENE by setting it to null, or otherwise leave us without a SCENE, it will close itself rather than throw an error (if your code is that bad though, you have more important things to worry about ). Secondly, it gives us a static method to quit the game. With the single line of code Game1.SCENE = null, we can end things on a dime.

Alright, so we at least want the game window to show up when we run. From here on out, when creating a new broad component to your game (a menu, a minigame, a title screen, etc.), make sure it will implement IScene!

    class Scene_TEMPIScene
    {

  //List your local variables here!
        public Scene_TEMP()
        {
//Use the constructor to initialize
//any local variables to your scene!

        }

        public void Update()
        {
//It's our update method!
        }

        public void Draw(SpriteBatch s_batch)
        {
  //Because the SpriteBatch passes by
//reference (it sends the pointer to 
//Game1.Spritebatch, not the contents of
//Spritebatch), we can just use s_batch
//send any calls to draw (and it's in-
//between the begin() / end() methods!
//No need to worry about that ever again!

        }
    }


This is just a basic template of a scene object we would need to create. In our Game1.Initialize() method, we simply need to specify what our initial SCENE object should be before the game officially runs. Because all input is ubiquitous and static, all components of our game are now broken down into two methods; Update and Draw. Game design could not be more straightforward.



If you can't already tell, the underlying philosophy of this guide has been "everything in one line of code". By coding objectively and creating a framework to handle more abstract tasks, we give ourselves more tools to work with. This concludes the first part of this guide. I hope you had fun, and please recommend this blog to your friends. Facebook it. Tweet it. Reddit it, so on. I'm a sucker for statistics, and the more people reading this the more inclined I am to update it.

In the next part of this guide, we will learn how to beat the dreaded Spritebatch, and display our graphics in a single line of code. Prepare for Part 2; Thinking Abstractly.

Homework: I'll soon provide the entirety of the code we should have, so the solution for this homework will be found there. In the Game1.Update() method, set the SCENE to the same SCENE object the initialize method sets whenever the F12 button is pressed (Think of this as a reset button. When F12 is pushed, the game jumps back to the title screen. The actual operations for a reset button should be more complex than this, but for now this will do).

Yesterdays Homework Solution: The code wasn't hard, just repetitive.

public static void BGM_play(string name)
{
    if(BGM != null) BGM.Stop(); 
    s_bgm = Player.Load<SoundEffect>(@"BGM\" + name);
    BGM = s_bgm.CreateInstance();
    BGM.IsLooped = true;
    BGM.Play();
}


public static void BGS_play(string name)
{
    if(BGS != null) BGS.Stop(); 
    s_bgs = Player.Load<SoundEffect>(@"BGS\" + name);
    BGS = s_bgs.CreateInstance();
    BGS.Volume = 0.8f;
    BGS.IsLooped = true;
    BGS.Play();
}


public static void SE_play(string name)
{
    if(SE != null) SE.Stop();
    s_se = Player.Load<SoundEffect>(@"SE\" + name);
    SE = s_se.CreateInstance();
    SE.Volume = 0.8f;
    SE.Play();
}


public static void ME_play(string name)
{
    if(ME != null) ME.Stop(); 
    s_me = Player.Load<SoundEffect>(@"ME\" + name);
    ME = s_me.CreateInstance();
    BGM.Pause();
    BGS.Pause();
    ME.Play();
    while(ME.state != stopped)
    {
    //this will resume the BGM and BGS
    //once the ME has completed.
    //Avoid using while statements like this though;
    //They're bad design, and they slow down the game
    //And in theory, this shouldn't even work at all.
    }
    BGM.Resume();
    BGS.Resume();
}


6 comments:

  1. I have a question about this lesson. I created a scene for my main menu...but I cannot load any pictures because when I try to load in the picture for my main screen's background outside of the game1 class...I get an error.

    Example: I created a new class that inherits the IScene interface. And in that class when I do this:

    background = Content.Load("blahblah");

    I get an error.

    ReplyDelete
  2. @Markimcomewitit
    It always helps to say what kind of error it was that you got. Still, I can tell the reason why is because Content is located in the game1 class, and cannot be used in your scene class.

    We'll be covering how to do this better soon, but for now create a new ContentManager for your Scene class.

    public static ContentManager Content =
    new ContentManager(Game1.services, "Content/");

    You then should be all set ^_^

    ReplyDelete
  3. Whoops, you might not want to make that static. My bad.

    ReplyDelete
  4. Yeah, I had just Googled it and found that that was the reason why >.< I gotta say this: I'm loving these tutorials. I just found them yesterday. A lot of these things I knew but this lesson right here is jusst exactly what I was looking for. I've been F5'ing this website for a while waiting for the abstraction lessons! I'm glad you're doing this :)

    ReplyDelete
  5. Great tuts! am going to follow these surely.

    Still i spotted a little bug, "Inconsistent accessibility: field type 'Tutorial.IScene' is less accessible than field 'Tutorial.Game1.SCENE'"

    This can easily be solved by changing "interface IScene" to "public interface IScene"

    ReplyDelete
  6. Huh, wouldya look at that? I must have missed that. Some things do get lost in translation when copying the code from the project to the blog, so minor things like that tend to happen.

    Nice call!

    ReplyDelete