Game Development Community

Multiple Levels Working

by Victor Ferenzi · in Torque X Platformer Kit · 02/16/2009 (10:36 am) · 10 replies

Below is a seires of steps I went through to get all the GUI graphics and scene objects and interaction to work accross multiple levels. The PSK seems to have an issue when you just use the SceneLoader.Load and SceneLoader.Unload methods. On major issue is that in the FontRender class of the Torque X assembly the _fontBatch reference throws a Object Disposed error when it is trying to issue the .End method of the XNA Sprite Batch object. Not sure if this problem exists in a Torque X game?

I started a new Platformer Starter game and removed all references to Torque X and the PlatformerFramework. I then copied my Torque X Engine code and the PlatformerFramework code over and re-referenced them in the project. This allowed for the debugging of the two assemblies.

I then started TGBX and copied the sample level scenes over as well as creating a new scene with Tilemaps and pickups. You can use whatever you scenes you have available to see if this works for you.

After that I started to address the Mutiple Level Load issues. Here are the steps.

1) Create this class in you game project. You can ofcourse use whatever GUI controls you need.
Most of these are the ones used in the PlatformerDemo project.

class PlayScene : GUISceneview, IGUIScreen
{
private GUIText _livesText;
private GUIBitmap _livesImage;
private GUIBitmap _livesXImage;

private GUIText _cashText;
private GUIBitmap _cashImage;
private GUIBitmap _cashXImage;

private GUIBitmap _pepperBox;
private List<GUIBitmap> _peppers = new List<GUIBitmap>();
private List<GUIBitmap> _pepperGhosts = new List<GUIBitmap>();

public PlayScene(T2DSceneCamera camera)
{
GUIStyle playStyle = new GUIStyle();
playStyle.IsOpaque = false;

GUISceneview play = new GUISceneview();
play.Name = "PlayScreen";
play.Style = playStyle;
play.Camera = camera;
play.HorizSizing = HorizSizing.Width;
play.VertSizing = VertSizing.Height;
play.Position = new Vector2(0.0f, 0.0f);
play.Size = new Vector2(1024.0f, 768.0f);

GUIBitmapStyle imageStyle = new GUIBitmapStyle();
imageStyle.HasBorder = false;

GUITextStyle lifeCountTextStyle = new GUITextStyle();
lifeCountTextStyle.FontType = @"data\font\LivesFont";
lifeCountTextStyle.TextColor[CustomColor.ColorBase] = Color.White;
lifeCountTextStyle.SizeToText = true;
lifeCountTextStyle.Alignment = TextAlignment.JustifyLeft;

GUITextStyle cashCountTextStyle = new GUITextStyle();
cashCountTextStyle.FontType = @"data\font\LivesFont";
cashCountTextStyle.TextColor[CustomColor.ColorBase] = Color.White;
cashCountTextStyle.SizeToText = true;
cashCountTextStyle.Alignment = TextAlignment.JustifyLeft;

_livesText = new GUIText();
_livesText.Name = "LivesText";
_livesText.Position = new Vector2(150.0f, 47.0f);
_livesText.Style = lifeCountTextStyle;
_livesText.Text = "0";
_livesText.Visible = true;

_livesXImage = new GUIBitmap();
_livesXImage.Style = imageStyle;
_livesXImage.Position = new Vector2(114.0f, 62.0f);
_livesXImage.HorizSizing = HorizSizing.Right;
_livesXImage.VertSizing = VertSizing.Bottom;
_livesXImage.Bitmap = @"data\images\HUD_x.png";
_livesXImage.Visible = true;

_livesImage = new GUIBitmap();
_livesImage.Style = imageStyle;
_livesImage.Position = new Vector2(56.0f, 44.0f);
_livesImage.HorizSizing = HorizSizing.Right;
_livesImage.VertSizing = VertSizing.Bottom;
_livesImage.Bitmap = @"data\images\HUD_extralives.png";
_livesImage.Visible = true;

_livesText.Folder = play;
_livesXImage.Folder = play;
_livesImage.Folder = play;

_cashText = new GUIText();
_cashText.Name = "CashText";
_cashText.Position = new Vector2(350.0f, 47.0f);
_cashText.Style = cashCountTextStyle;
_cashText.Text = "0";
_cashText.Visible = true;

_cashXImage = new GUIBitmap();
_cashXImage.Style = imageStyle;
_cashXImage.Position = new Vector2(314.0f, 62.0f);
_cashXImage.HorizSizing = HorizSizing.Right;
_cashXImage.VertSizing = VertSizing.Bottom;
_cashXImage.Bitmap = @"data\images\HUD_x.png";
_cashXImage.Visible = true;

_cashImage = new GUIBitmap();
_cashImage.Style = imageStyle;
_cashImage.Position = new Vector2(256.0f, 44.0f);
_cashImage.HorizSizing = HorizSizing.Right;
_cashImage.VertSizing = VertSizing.Bottom;
_cashImage.Bitmap = @"data\images\treasure\SonicRing.png";
_cashImage.Visible = true;

_cashText.Folder = play;
_cashXImage.Folder = play;
_cashImage.Folder = play;

_pepperBox = new GUIBitmap();
_pepperBox.Style = imageStyle;
_pepperBox.Position = new Vector2(636.0f, 34.0f);
_pepperBox.HorizSizing = HorizSizing.Left;
_pepperBox.VertSizing = VertSizing.Bottom;
_pepperBox.Bitmap = @"data\images\HUD_chilibox.png";
_pepperBox.Visible = true;

_pepperBox.Folder = play;

// get base pepper position
Vector2 pepperPosition = new Vector2(_pepperBox.Position.X + 20, _pepperBox.Position.Y + 10);
float pepperOffset = 30.0f;

// place grey empty peppers
// (visible)
for (int i = 9; i >= 0; i--)
{
GUIBitmap pepperGhost = new GUIBitmap();
pepperGhost.Style = imageStyle;
pepperGhost.Position = new Vector2(pepperPosition.X + (i * pepperOffset), pepperPosition.Y);
pepperGhost.HorizSizing = HorizSizing.Left;
pepperGhost.VertSizing = VertSizing.Bottom;
pepperGhost.Bitmap = @"data\images\HUD_pepperGhost.png";
pepperGhost.Visible = true;
pepperGhost.Folder = play;
_pepperGhosts.Add(pepperGhost);
}

// place red full peppers
// (invisible)
for (int i = 9; i >= 0; i--)
{
GUIBitmap pepper = new GUIBitmap();
pepper.Style = imageStyle;
pepper.Position = new Vector2(pepperPosition.X + (i * pepperOffset), pepperPosition.Y + 2);
pepper.HorizSizing = HorizSizing.Left;
pepper.VertSizing = VertSizing.Bottom;
pepper.Bitmap = @"data\images\HUD_peppers.png";
pepper.Visible = false;
pepper.Folder = play;
_peppers.Add(pepper);
}

// create game gui (important to do this as the last step)
GUICanvas.Instance.SetContentControl(play);
}
}

2) Add these methods to you game.cs file. You can do whatever pre and post setups necessary for you game.
Again these are mainly from the PlatformerDemo project.

/// <summary>
/// Delegate callback when the game's level data has finished being deserialized.
/// </summary>
/// <param name="sceneFile">The filename of the level data.</param>
/// <param name="scene">The level data object containing the deserialized file.</param>
public void OnSceneLoaded(string sceneFile, TorqueSceneData scene)
{
// set the parallax manager's target object
T2DSceneCamera camera = TorqueObjectDatabase.Instance.FindObject<T2DSceneCamera>("Camera");
ParallaxManager.Instance.ParallaxTarget = camera;

// set the scene graph
_sceneGraph = camera.SceneGraph as T2DSceneGraph;

//jump to the new play gui instead of the default sceneview
_playGui = new PlayScene(camera);
}

/// <summary>
/// Delegate callback when the game's level data is being unloaded.
/// </summary>
/// <param name="sceneFile">The filename of the level data.</param>
/// <param name="scene">The level data object containing the deserialized file.</param>
public void OnSceneUnloaded(string sceneFile, TorqueSceneData scene)
{
//jump to the new play gui instead of the default sceneview
T2DSceneCamera camera = TorqueObjectDatabase.Instance.FindObject<T2DSceneCamera>("Camera");
_playGui = new PlayScene(camera);
}

3) In your game.cs in BeginRun add in the lines of code to attach the OnSceneLoaded and OnSeneUnloaded events.

protected override void BeginRun()
{
base.BeginRun();

// set the callback events for load and unload
SceneLoader.OnSceneLoaded = this.OnSceneLoaded;
SceneLoader.OnSceneUnloaded = this.OnSceneUnloaded;

// load the test level
SceneLoader.Load(@"data\levels\sample_level.txscene");

// initialize the sound system
InitSound();

// game start
_gameStart = Time;
}


In doing the following I now can toggle between the test scenes I have created with not apparent issues.
The real test begins when I start to incorporate the game scenes I created which are somewhat more
complexed. If there are any issues with this approach these scenes should flush them out.

Please do you own testing and send feedback. The PSK is a nice to have but without the levels
loading and unloading functionally the kit does not seem like an attrative option for game development.

Victor

#1
02/25/2009 (12:15 pm)
Update to these steps:

In running through the steps outlined above I discovered that this piece of code still needed to be implemented:

Add the following to the _OnRegister event for that class.

// reset the camera if null (Add this above the _initAnimationManager)
if (this.SceneObject.SceneGraph.Camera == null)
{
this.SceneObject.SceneGraph.Camera =
TorqueObjectDatabase.Instance.FindObject<T2DSceneCamera>("Camera");
}

// initialize animation manager
// (this will handle all our animations and transitions)
_initAnimationManager();

Once I did that the Null Reference Exception that occured from one scene to another was fixed.

Victor
#2
03/10/2010 (10:09 pm)
Hello..

How much would you charge $$ to implement this into my project.. Its a stock platformer x kit.. with level changing issues.. Let me know..
#3
05/04/2010 (9:12 pm)
hello, hope all are well.

@Victor - ok,... im still a bit confused, but i wanted to ask would any of this method work if i wanted to use the GoldEggCollectibleComponent.cs in the PSK as the trigger for the end of level to unload and load the next level? what changes would i need to make or how would this be added?

thanks for your time, have a great day.
#4
05/05/2010 (8:45 am)
Here (warning, not all those steps might be essential, I basically added some until it worked and now I'm slowly removing them):

add this in Game.cs:
public string ChangeLevelDirty
{
get { return _changeLevelDirty; }
set { _changeLevelDirty = value; }
}


change update in Game.cs to this:
protected override void Update(GameTime gameTime)
{
if (_changeLevelDirty != null)
{
ChangeLevel(_changeLevelDirty);
}
_changeLevelDirty = null;

base.Update(gameTime);

totalTime += gameTime.ElapsedGameTime.Milliseconds;
}


then, add this function, still in Game.cs:
protected void ChangeLevel(string sceneFileName)
{
if (_currentScene != null)
{
SceneLoader.UnloadLastScene();
}

if (PlatformerDemoGUI.Instance.GUI.Folder != null)
{
PlatformerDemoGUI.Instance.GUI.Folder.RemoveAllValues();
}

SceneLoader.Load(sceneFileName);
_currentScene = sceneFileName;

// set the parallax manager's target object
T2DSceneCamera camera = TorqueObjectDatabase.Instance.FindObject<T2DSceneCamera>("Camera");
ParallaxManager.Instance.ParallaxTarget = camera;
_sceneGraph = camera.SceneGraph as T2DSceneGraph;

// create game gui
playStyle = new GUIStyle();
play = new GUISceneview();
play.Name = "PlayScreen";
play.Style = playStyle;
play.Camera = camera;
_sceneGraph.Camera = camera;

GUICanvas.Instance.SetContentControl(play);
PlatformerDemoGUI.Instance.GUI.Folder = GUICanvas.Instance;

T2DSceneObject player = TorqueObjectDatabase.Instance.FindObject<T2DSceneObject>("Player");
camera.Mount(player, "playerCamera", new Vector2(0, 0), 0, false);
}


Now, still in Game.cs, change BeginRun to this:
protected override void BeginRun()
{
base.BeginRun();

_currentScene = null;
_changeLevelDirty = null;
ChangeLevel(@"datalevelslevel_01.txscene");

// register sound groups
SoundManager.Instance.RegisterSoundGroup("music", @"datasoundmusic.xwb", @"datasoundmusic.xsb");
SoundManager.Instance.RegisterSoundGroup("general", @"datasoundgeneral.xwb", @"datasoundgeneral.xsb");
SoundManager.Instance.RegisterSoundGroup("drill", @"datasounddrill.xwb", @"datasounddrill.xsb");
SoundManager.Instance.RegisterSoundGroup("dirt", @"datasounddragon.xwb", @"datasounddragon_dirt.xsb");
SoundManager.Instance.RegisterSoundGroup("grass", @"datasounddragon.xwb", @"datasounddragon_grass.xsb");
SoundManager.Instance.RegisterSoundGroup("wood", @"datasounddragon.xwb", @"datasounddragon_wood.xsb");

// grab the sound categories
_soundEffectsCategory = Engine.SFXDevice.GetCategory("Default");
_musicCategory = Engine.SFXDevice.GetCategory("Music");

// game start
_gameStart = Time;

// start music loop
_indoorMusic = "indoor_music";
_outdoorMusic = "outdoor_music";
_levelCompletedMusic = "finish_level";
_musicCueName = _outdoorMusic;
_musicSoundGroup = "music";
_music = SoundManager.Instance.PlaySound(_musicSoundGroup, _outdoorMusic);

// play the dragon's startup sound for the first time
SoundManager.Instance.PlaySound("grass", "startup");
}

and finally, still in Game.cs, add this:
string _currentScene;
string _changeLevelDirty;
#5
05/05/2010 (8:46 am)

Now, I have added this component to my game:
using System;
using System.Collections.Generic;
using System.Text;

using Microsoft.Xna.Framework;

using GarageGames.Torque.Core;
using GarageGames.Torque.T2D;

using GarageGames.Torque.PlatformerFramework;
using GarageGames.Torque.GameUtil;

namespace GarageGames.Torque.PlatformerDemo
{
/// <summary>
/// This component is an example of how to use the CollectibleComponent to create a Collectible that is added
/// to an inventory when picked up.
/// </summary>
[TorqueXmlSchemaType]
class SwitchLevelCollectible : CollectibleComponent
{
//======================================================
#region Public properties, operators, constants, and enums

public string LevelToLoad
{
get { return _levelToLoad; }
set { _levelToLoad = value; }
}

/// <summary>
/// If ResolveCollision is set to this static delegate then collisions
/// will result in a bounce with no rotation. The physics material will be used
/// to determine the amount of bounce.
/// </summary>
static public T2DResolveCollisionDelegate BounceCollisionTest
{
get { return _bounceCollisionTest; }
}

#endregion

//======================================================
#region Private, protected, internal methods

protected override bool _confirmPickup(T2DSceneObject ourObject, T2DSceneObject theirObject, ActorComponent actor)
{
// try to add this switchlevel to the Dragon's inventory
if (actor is PlayerDragonActorComponent)
{
// check if this object was spawned
if (ourObject.TestObjectType(PlatformerData.SpawnedObjectType))
{
// check for a spawned object component and notify it that we don't want to respawn
CheckpointSystemSpawnedObjectComponent spawned = ourObject.Components.FindComponent<CheckpointSystemSpawnedObjectComponent>();

if (spawned != null)
{
spawned.Recover = false;
}
}

// update the pepper count in the HUD
if (_levelToLoad != null)
{
PlatformerDemo.Game.Instance.ChangeLevelDirty = _levelToLoad;
}

// true = yes, i was picked up. delete me!
return true;
}

// false = no, this guy didn't pick me up.
return false;
}

#endregion

//======================================================

#region Stock collision response methods

static protected void _resolveBounceTest(T2DSceneObject ourObject, T2DSceneObject theirObject, ref T2DCollisionInfo info, T2DCollisionMaterial physicsMaterial, bool handleBoth)
{
if (ourObject.Physics != null)
{
// are we already bouncing away?
float dot = Vector2.Dot(ourObject.Physics.Velocity, info.Normal);
if (dot < 0.0f)
ourObject.Physics.Velocity = ourObject.Physics.Velocity - (1.0f + physicsMaterial.Restitution) * dot * info.Normal;
}
if (handleBoth && theirObject.Physics != null)
{
// are they already bouncing away?
float dot = Vector2.Dot(theirObject.Physics.Velocity, -info.Normal);
if (dot < 0.0f)
theirObject.Physics.Velocity = theirObject.Physics.Velocity + (1.0f + physicsMaterial.Restitution) * dot * info.Normal;
}
}

#endregion

//======================================================

#region Private, protected, internal fields

static protected T2DResolveCollisionDelegate _bounceCollisionTest = new T2DResolveCollisionDelegate(_resolveBounceTest);

protected string _levelToLoad;

#endregion
}
}


but you can use the :
PlatformerDemo.Game.Instance.ChangeLevelDirty = _levelToLoad;
syntax to change it anywhere else arbitrarily.


It should give you a clean change and require NO OTHER FIX than those mentioned here. HOWEVER, I am using the svn version of both Torque X and Torque X 2D, so this might alter behavior, albeit I doubt it. If it doesn't work, you should look in the Torque X 2D forum for the Memory Leak thread with a huge (like over 200) post count and ask inclusion in the svn there, everyone has been very kind and quick up to now, I see no reason for this to change :)

Now, I plan to offer more control on this LevelChange (like having multiple scenes loaded at once), but this simple mechanic should work for most projects.

Hope this helps :)


P.S.: Sorry for the double post, they have a limit on characters and words ;)
#6
05/05/2010 (9:06 am)
The svn version of Torque X has some fixes for managing scenegraphs, but if you only have one level loaded at a time then this should not cause any issues anyway.


@Olivier: If you choose to load multiple scene files at the same time (which I do), then you will need to be careful where you get your reference for the camera. The safest place is to get it from the scenedata object that SceneLoader.Load() returns, as the database will have multiple cameras called 'Camera' if there are multiple scenes loaded (and you have no garrauntee you will get the right one when you ask the database for it).

Also if you are using the svn version you will need to make sure that you move all objects to the correct scenegraph after loading a scene file - if you want the scenes to 'merge' then all the loaded objects should be in the same scenegraph. See the async loader wiki and some of the posts in the community svn project forums for further examples/discussions.
#7
05/05/2010 (9:41 am)
Ah, well, seeing as I was planning to use one scene for the hud and another for the game, I don't think it'll be necessary (except with the camera trick, which I solved by removing the camera from the hud scene), but thanks for pointing it out, that way if I put some code for others to use, I can make sure to worry about that :)

Thank you.
#8
05/05/2010 (10:07 am)
Using a second scene for the HUD was covered in a thread recently although I can't remember which one.
#9
05/05/2010 (10:10 am)
Ah, this one: www.torquepowered.com/community/forums/viewthread/114317


Note that you only need to do this if you are using things besides the Torque X GUI objects in your HUD. Otherwise you only need one scene (the GUI stuff is already designed to work independent of loaded scenes).
#10
05/05/2010 (10:13 am)
Indeed it is, however, I like to give as much leeway to my level designers as possible, thus I thought of adding the possibility as cleanly and simply as possible was better than wait for them to ask for something similar. They can be quite surprising sometimes with more tools or even small variation of the same one :)

Thanks for the link, btw.