In this chapter, we are going to focus on the Tool Box part of the editor. This toolbox will be a tree of categories and the components available in them, which will be constructed by scanning for components in the assemblies linked to by the game’s .csproj that we are working with.

To start, add a TreeView to your form, and call it toolboxTree. Second, add a MenuStrip to the form. It should stick itself to the top of the form. Now click where it says “Type Here”, and type “Tools”. This will add a new button to the menu strip. Click on it, and repeat the process in the dropdown by adding a “Link XNA Game” button. Double click on the “Link XNA Button” now, and you will be taken to its “Click” event. Before we start writing code here, we need a place to keep track of what game we are working with. Create a new class in another file called “EditorGlobals.cs”.

// Keeps track of global variables used to keep track of editor state
public static class EditorGlobals
{
    // Path to the .csproj project of the linked game
    static string gameFilePath;

    static string currentSceneFileName = null;

    // Class that watches a directory and notifies us of changes. For
    // example, new content being built or the game project being updated
    static FileSystemWatcher watcher;

    // Events for when the game project is about to change
    // and has changed
    public static event EventHandler LinkedGameChanging;
    public static event EventHandler LinkedGameChanged;
    public static event FileSystemEventHandler GameDirectoryModified;

    // Path to the .csproj project of the linked game
    public static string GameFilePath
    {
        get { return gameFilePath; }
        set
        {
            // Fire LinkedGameChanging to notify the editor
            // that the path is about to change
            if (LinkedGameChanging != null)
                LinkedGameChanging(null, new EventArgs());

            // Set the new path
            gameFilePath = value;

            currentSceneFileName = null;

            // Fire LinkedGameChanging to notify the editor
            // that the path has changed
            if (LinkedGameChanged != null)
                LinkedGameChanged(null, new EventArgs());

            // Rebuild the FileSystemWatcher with the new directory
            watcher = new FileSystemWatcher(GameDirectory);
            watcher.IncludeSubdirectories = true;

            // We must make the watcher use our thread to avoid illegal
            // cross thread operation
            watcher.SynchronizingObject = Form1.ActiveForm;

            watcher.Changed += new FileSystemEventHandler(watcher_Changed);
            watcher.Deleted += new FileSystemEventHandler(watcher_Changed);

            // Make sure the watcher doesn't fire until we are done with the
            // project's directories
            watcher.EnableRaisingEvents = false;

            // Make sure the scene directory exists
            if (!Directory.Exists(GameSceneDirectory))
                Directory.CreateDirectory(GameSceneDirectory);

            // Make sure the content directory exists
            if (!Directory.Exists(GameContentDirectory))
                Directory.CreateDirectory(GameContentDirectory);

            Engine.Content.RootDirectory = GameDirectory + "bin\\x86\\Debug\\";

            // Re-enable the watcher
            watcher.EnableRaisingEvents = true;

            // Fire the GameDirectoryModified event through watcher_Changed
            watcher_Changed(null, new FileSystemEventArgs(
                WatcherChangeTypes.Changed, GameDirectory, Path.GetDirectoryName(value)));
        }
    }

    // Called by the FileSystemWatcher when the game directory is modified
    static void watcher_Changed(object sender, FileSystemEventArgs e)
    {
        // Simply forward the event to GameDirectoryModified
        if (GameDirectoryModified != null)
            GameDirectoryModified(sender, e);
    }

    // Whether or not a game has been linked to the editor
    public static bool EditorLinkedToGame
    {
        get { return gameFilePath != null; }
    }

    // The directory of the game's project
    public static string GameDirectory
    {
        get { return Path.GetDirectoryName(gameFilePath) + "\\"; }
    }

    // The directory of the game's built content
    public static string GameContentDirectory
    {
        get { return GameDirectory + "bin\\x86\\Debug\\Content"; }
    }

    public static string GameSceneDirectory
    {
        get { return GameDirectory + "bin\\x86\\Debug\\Scenes"; }
    }

    public static string CurrentSceneName
    {
        get { return currentSceneFileName; }
        set { currentSceneFileName = value; }
    }

    public static string CurrentSceneFilePath
    {
        get { return GameSceneDirectory + "\\" + currentSceneFileName + ".iescn"; }
    }
}

There is a lot of stuff in there we won’t be using yet, but it’s easiest just to write it all now. This class is basically a container for us to store a bunch of values used all over the editor, so we can get to them through EditorGlobals.XXXXX, instead of passing stuff around. These include settings like the current directory of the game’s scenes, content, the game projects filename and directory, etc. It also takes care of monitoring these directores and firing an event when they change. One important property here the the file path to the game’s .csproj file. When this is changed, we do a lot of work resetting our FileSystemWatcher, checking if all the necessary directories exists, etc.

Now we can get back to “Link XNA Game” button’s event handler. We are simply going to ask for a filename for an XNA game .csproj, then set the GameFilePath property above and trigger all the code mentioned above.

private void linkXNAGameToolStripMenuItem_Click(object sender, EventArgs e)
{
    // Reset the editor
    ResetScene(true);

    // Create a file selection window
    OpenFileDialog ofd = new OpenFileDialog();
    ofd.Filter = "XNA Game Project|*.csproj";

    // Set the new projected to the selection
    if (ofd.ShowDialog() == DialogResult.OK)
        EditorGlobals.GameFilePath = ofd.FileName;
}

// Resets the editor back to its initial state
void ResetScene(bool CheckForSave)
{
    if (EditorGlobals.EditorLinkedToGame && CheckForSave)
    {
        DialogResult result = MessageBox.Show("The current scene is about to be unloaded. Would you like to save it first?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);

        if (result == DialogResult.Cancel)
            return;
        else if (result == DialogResult.Yes)
        {
            string sceneName = Path.GetFileNameWithoutExtension(
                TextDialog.Show("Scene Name", "Choose a name for the scene", !string.IsNullOrEmpty(EditorGlobals.CurrentSceneName) ? EditorGlobals.CurrentSceneName : ""));

            if (sceneName != null)
                SaveScene(sceneName);
        }
    }

    Engine.Reset();

    RenderControl.AddDefaultComponents();
}

For this to work, the initialization code for the camera, keyboard, and mouse must be moved in the RenderControl class to its own method.

public static void AddDefaultComponents()
{
    // Setup the camera and move it to a good position, and make it a service
    Camera = new FPSCamera();
    Camera.RotateTranslate(new Vector3(0, 0, 0), new Vector3(128, 15, 300));
    Engine.Services.AddService(typeof(Camera), Camera);

    // Setup the keyboard and mouse, and allow the cursor to move
    Keyboard = new KeyboardDevice();
    Mouse = new MouseDevice();
    Mouse.ResetMouseAfterUpdate = false;

    Camera.Serialize = false;
    Keyboard.Serialize = false;
    Mouse.Serialize = false;
}

We also need a method to save the current scene in the main form:

void SaveScene(string Name)
{
    Engine.SerializeState(EditorGlobals.GameSceneDirectory + "\\" + Name + ".iescn");
    EditorGlobals.CurrentSceneName = Name;
}

The ResetScene method also makes use of another class, which simply displays a TextBox on a popup. Here are the code files for that windows form. (I don’t want to get into it, because it’s not exactly relevent, but we still need it.)

TextDialog.cs
TextDialog.Designer.cs
TextDialog.resx

That’s all we are going to do this time, but in the next tutorial we will add the code that populates the toolbox TreeView when a new project is linked.

« »