简体   繁体   中英

Sprites, XNA, C# and elegance

I'm back. Again. :3 Right now, I'm working on my RPG project (just for fun, I don't have any illusions that it will be easy), and I've come to the point where I've written the underlying framework and I now want to write an elegant method of drawing sprites from a sprite map.

At the moment I am using a sprite map from pokémon diamond (just to test, because it was easily available. I am not making the next "pokemon" game), which has the main hero walking in all three directions on one row, 37px x 37px sprites, 12 sprites in the image.

The image is here: http://spriters-resource.com/ds/pkmndiamondpearl/lucas.png (I am working with the "Walking" subset, currently).

I have created SpriteSheet class (along with a SpriteSheetData class, which is a representation of an XML file for a collection of spritesheets) and a SpriteManager class, all of which are listed below:

SpriteSheet.cs

namespace RPG.Utils.Graphics
{
/// <summary>
/// Represents a Sprite Sheet. A Sprite Sheet is a graphic that contains a number of frames for animations.
/// These are laid out a set distance apart.
/// </summary>
public struct SpriteSheet
{
    /// <summary>
    /// The name for the texture. Used internally to reference the texture.
    /// </summary>
    [ContentSerializer]
    public string TextureName
    {
        get;
        private set;
    }
    /// <summary>
    /// The file name of the texture.
    /// </summary>
    [ContentSerializer]
    public string TextureFile
    {
        get;
        private set;
    }
    /// <summary>
    /// The width of each sprite in the sprite sheet.
    /// </summary>
    [ContentSerializer]
    public int SpriteWidth
    {
        get;
        private set;
    }
    /// <summary>
    /// The height of each sprite in the sprite sheet.
    /// </summary>
    [ContentSerializer]
    public int SpriteHeight
    {
        get;
        private set;
    }
    /// <summary>
    /// The interval between each frame of animation.
    /// This should be (by default) 100f or 100ms.
    /// </summary>
    [ContentSerializer]
    public float AnimationInterval
    {
        get;
        set;
    }

    /// <summary>
    /// The number of frames per each individual animation.
    /// </summary>
    [ContentSerializer]
    public int AnimationLength
    {
        get;
        set;
    }

    /// <summary>
    /// The texture for this sprite sheet.
    /// </summary>
    [ContentSerializerIgnore]
    public Texture2D Texture
    {
        get;
        set;
    }
}

SpriteManager.cs

/// <summary>
/// A sprite manager. Just loads sprites from a file and then stores them.
/// </summary>
public static class SpriteManager
{
    private static Dictionary<string, SpriteSheetData> m_spriteSheets;
    public static Dictionary<string, SpriteSheetData> SpriteSheets
    {
        get
        {
            if (m_spriteSheets == null)
                m_spriteSheets = new Dictionary<string, SpriteSheetData>();
            return m_spriteSheets;
        }
    }
    /// <summary>
    /// Loads all the sprites from the given directory using the content manager.
    /// Sprites are loaded by iterating SpriteSheetData (.xml) files inside the /Sprites/ directory.
    /// </summary>
    /// <param name="mgr">Content Manager.</param>
    /// <param name="subdir">Directory to load.</param>
    public static void LoadAllSprites(ContentManager mgr, string subdir)
    {
        // Get the files in the subdirectory.
        IEnumerable<string> files = Directory.EnumerateFiles(mgr.RootDirectory+"/"+subdir);
        foreach (string f in files)
        {
            // Microsoft, why do you insist on not letting us load stuff with file extensions?
            string fname = f.Replace("Content/", "").Replace(".xnb", "");
            SpriteSheetData data = mgr.Load<SpriteSheetData>(fname);

            string spriteSheetDir = subdir +"/" + data.SpriteSheetName + "/";

            int loaded = 0;
            for (int i = 0; i < data.SpriteSheets.Length; i++)
            {
                loaded++;
                SpriteSheet current = data.SpriteSheets[i];
                current.Texture = mgr.Load<Texture2D>(spriteSheetDir + current.TextureFile);
                data.SpriteSheetMap[current.TextureName] = current;
            }

            Console.WriteLine("Loaded SpriteSheetData file \"{0}\".xml ({1} sprite sheet(s) loaded).", data.SpriteSheetName, loaded);
            SpriteSheets[data.SpriteSheetName] = data;
        }
    }

    /// <summary>
    /// Query if a given Sprite definition file is loaded.
    /// </summary>
    /// <param name="spriteName">
    /// The sprite definition file name (ie, "Hero"). This should correspond with the XML file
    /// that contains the definition for the sprite sheets. It should NOT be the name OF a spritesheet.
    /// </param>
    /// <returns>True if the sprite definition file is loaded.</returns>
    public static bool IsLoaded(string spriteName)
    {
        return SpriteSheets.ContainsKey(spriteName);
    }
}

SpriteSheetData.cs

/// <summary>
/// Represents data for a SpriteSheet. These are stored in XML files.
/// </summary>
public struct SpriteSheetData
{
    /// <summary>
    /// The collective name for the sprite sheets.
    /// </summary>
    [ContentSerializer]
    public string SpriteSheetName
    {
        get;
        set;
    }
    /// <summary>
    /// The SpriteSheets in this data file.
    /// </summary>
    [ContentSerializer]
    internal SpriteSheet[] SpriteSheets
    {
        get;
        set;
    }


    [ContentSerializerIgnore]
    private Dictionary<string, SpriteSheet> m_map;
    /// <summary>
    /// The sprite sheet map.
    /// </summary>
    [ContentSerializerIgnore]
    public Dictionary<string, SpriteSheet> SpriteSheetMap
    {
        get
        {
            if (m_map == null)
                m_map = new Dictionary<string, SpriteSheet>();
            return m_map;
        }
    }
}

And the file that I am using to "read in" the sprites is: Sprites/Hero.xml

<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
  <Asset Type="RPG.Utils.Graphics.SpriteSheetData">
    <SpriteSheetName>Hero</SpriteSheetName>
    <SpriteSheets>
      <Item Type="RPG.Utils.Graphics.SpriteSheet">
        <TextureName>HeroWalking</TextureName>
        <TextureFile>hero_walk</TextureFile>
        <SpriteWidth>37</SpriteWidth>
        <SpriteHeight>37</SpriteHeight>
        <AnimationInterval>400</AnimationInterval>
        <AnimationLength>3</AnimationLength>
      </Item>
    </SpriteSheets>
  </Asset>
</XnaContent>

The issue I am having is that I am unsure of a way to elegantly organize 1) the sprite loading and 2) the pairing of a sprite sheet (this is the "generic" part of the game, the bit I intend on re-using, so I don't know HOW many instances of an entity I may create) to an entity/ingame object - particularly the player (since that is what I am attempting to do now). I am unsure of how to continue so any help is GREATLY appreciated.

If you need any more code (God forbid) ask and it shall be given unto you :3

Organizing wise, I've always loved doing it one particular way, and the structure you've used lends itself fairly well towards it.

I like to load my objects like so:

  1. Have a level file (XML or TXT) that describes what should be loaded for X task/level.
  2. Iterate through this with a loop, each of the objects to be loaded each have their own file (much like your Hero.xml).
  3. Each of these is loaded via a level manager that handles loading of all the data.
  4. Draw and proceed to run the level through the level manager.

Then it's largely up to you how you call your level manager and implement it. I like to use these level configuration files, as well as level managers, because then you can share resources throughout the level manager. So, you can use referenced textures from within the level manager with a dictionary and pass those down to your game objects within your loaded level, this way you don't have multiples of loaded textures.

From what I know of in C#, the "pass by reference thing" tends to prevent this from being an issue, but I come from the C++ arena, where this kind of content management for your level is a great idea.

As for your player, you can create a player manager class that interfaces with your level managers, so that the player can remain persistent in-between levels and loading.

This is how I organize it anyways.

Cheers!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM