JOSEPH SIEREJKO

GAME DEVELOPER : PROGRAMMER : DESIGNER

Red Rock Farmer
roles Lead Programmer • 2D Artist • UI
software MonoGame • Unity Engine • Adobe Photoshop • Visual Studio
languages C# • Visual C#
team size
Red Rock Farmer is a farming RPG where you are a part of the first group of colonies on Mars, with the goal to make the planet habitable, recruit new neighbors as you expand and build out a farm from the red soil. Initially, I built it for submission for my 10 week Senior Project during my Associate's Degree. It was first built using Unity, but I later ported it to monogame simply because I felt more comfortable programming my own tools purely in code. Though there are some distinct differences at the moment, I was able to redesign the code using the Monogame/XNA extension in 3 weeks. part of it due to 2d assets being finished. Functionally it plays the same, but improvements were made on a trailing camera, and I wrote UI scripts to handle screen and world space canvases, which were more tailored to my needs.
As the project is temporarily on hold, there are a plenty of iterations that need to be made, and a few placeholder sprites that need to be swapped out. All this to say, this is still in a prototype state.
Most of the gameplay mechanics in this Prototype layout the entire gameplay loop. There are plenty of missing factors to be implemented to round out the experience, but at its core, it is a farming RPG. It has a defined gameplay loop: Buy Seeds; Plant; Sleep; Harvest; Repeat.
Player Controller and GameObjects
  • Testing for collision is based on the corners of tiles. (Moving up tests intersections of either the bottom-right or bottom-left of the 2 tiles.
  • It is predictive movement, boiled down to the movement attempt using Current Speed and Position
    • A temporary rect with the next Movement coords, and if intersection occurs, the Velocity is set to 0 to the applicable axis.
  • The Player inherits from my own take on a GameObject class that by default has collision functions to compare to tiles
    • So adding enemies (which already has an A* pathfinder class and Tile Nodes ready to implement), would require only adding an AI brain.
Predictive collision code in the GameObject class. The Horizontal tests the left and right corners using the corner pixels outside of the GameObject's BoxCollider
GameObject.cs Snippet
Vector2 HorizontalCollisionTest(Vector2 movement)
{
    if (movement.X == 0) //No need to test collision on X Axis
        return movement;

    Rectangle moveTestCollider = BoxCollider;

    //Get the path of next movement by offsetting the collider
    moveTestCollider.Offset((int)movement.X, 0);
    Vector2 topPixel, bottomPixel;

    if (movement.X < 0)
    {
        topPixel = new Vector2(moveTestCollider.Left, moveTestCollider.Top + 1);        //Gets top left corner of movement
        bottomPixel = new Vector2(moveTestCollider.Left, moveTestCollider.Bottom - 1);  //Gets bottom left corner of movement
    }
    else
    {
        topPixel = new Vector2(moveTestCollider.Right, moveTestCollider.Top + 1);       //Gets top right corner of movement
        bottomPixel = new Vector2(moveTestCollider.Right, moveTestCollider.Bottom - 1); //Gets bottom right corner of movement
    }
    //Covers the 2 tiles, taking the possibility that player is jumping or falling (Y position)
    Vector2 topCell = TileMap.GetCellByPixel(topPixel);
    Vector2 bottomCell = TileMap.GetCellByPixel(bottomPixel);

    //Tests Cells for collision
    if (TileMap.IsColliding(topCell) || TileMap.IsColliding(bottomCell))
    {
        movement.X = 0;     //Disable movement on X
        velocity.X = 0;     //Cancel velocity for current frame
    }

    return movement;
}
In the TileMap, there are a number of Overloaded implementatations of Collision Testing. The Top Implementation is unimplemented, but is meant to be custom box colliders, allowing more precise collision
TileMap.cs Snippet
static public Rectangle CellWorldCollider(int cellX, int cellY)
{
    Tile tile = GetTileAtCell(cellX, cellY);

    if(tile != null)

    return new Rectangle(
        (cellX * TileWidth) + tile.boundX,
        (cellY * TileHeight) + tile.boundY,
        tile.boundWidth, tile.boundHeight);

    return new Rectangle(0,0,0,0);
}

//Collider ScreenSpace Rectangle for tile
static public Rectangle CellScreenCollider(int cellX, int cellY) =>
    Camera.WorldToScreen(CellWorldCollider(cellX, cellY));

//Given the pieces of a coordinate, return whether the Cell has active collision
static public bool IsColliding(int cellX, int cellY)
{
    Tile tile = GetTileAtCell(cellX, cellY);

    if (tile == null)
        return false;
    else
        return tile.CollisionEnabled;
}

static public bool IsCollidingByPixel(Vector2 pixelLocation) =>
    IsColliding(
    GetCellByPixelX((int)pixelLocation.X),
    GetCellByPixelY((int)pixelLocation.Y));

static public bool IsColliding(Vector2 cell) => IsColliding((int)cell.X, (int)cell.Y);
Inventory Manager
Inventory was one of the m ost time consuming products, both in Monogame and Unity. In its current form has a few important functions.
  • Each Slot class has a stack of Item classes to store a type of item
    • The stacks have flexibility and are easily moved from slot to slot
    • When a stack gets an item of a type, it becomes "Occupied" and the available slot count is updated
    • Everytime an item is added to slot, the inventory searches for the item by an ID.
      • If an item of type is found, the UIText in that slot updates its item count
      • Otherwise it places itself onto a new slot
      • A full inventory means...just ignore everything above
  • There is a reorganize function that also puts all items in the earliest slots, removing empty slots between items
  • Finally, of course, there is the Add, Remove, and Drop functions, which pop or push from a slot's stack
    • This is used in conjunction with the Tile Engine's Tile class to pass an Item to the Tile
The inventory code below is primarily Utility functions having to do with navigation and managing slot stacks. It is some of the more important code involving core gameplay
InventoryManager.cs Snippet
#region Selection
                                   
public static Slot GetSlot(int index) => Slots[index];

//Takes current index and moves it by a specified amount
static void TranslateToActiveSlot(int amount)
{
    SlotIndex += amount;
    CurrentSelectedSlot = GetSlot(SlotIndex);
    moveCurrentTime = 0f;
}

//Direcly Set the Current Slot
static void SetActiveSlot(int index)
{
    SlotIndex = index;
    CurrentSelectedSlot = GetSlot(SlotIndex);
    moveCurrentTime = 0f;
}

public static void HandleSlotSelection(GameTime gameTime)
{
    KeyboardState kState = Keyboard.GetState();

    //Timer - allows holding key down without moving every frame
    moveCurrentTime += (float)gameTime.ElapsedGameTime.TotalSeconds;

    if (moveCurrentTime < moveSelectionTimer) return;

    if (kState.IsKeyDown(Keys.Up) & SlotIndex - ColCount >= 0)
    {
        TranslateToActiveSlot(-ColCount);   //Move up a row
    }
    else if (kState.IsKeyDown(Keys.Down) & (SlotIndex + ColCount < Slots.Count))
    {
        TranslateToActiveSlot(ColCount);    //Move down a row
    }
    else if (kState.IsKeyDown(Keys.Left) & SlotIndex > 0)
    {
        TranslateToActiveSlot(-1);          //Move Left
    }
    else if (kState.IsKeyDown(Keys.Right) & (SlotIndex + 1 < Slots.Count))
    {
        TranslateToActiveSlot(1);           //Move Right
    }
}

#endregion

#region Add, Remove, and Manage Items

public static void AddItem(Item item)
{
    if (IsFull) return;

    //First loop checks for item by ID in reduced list
    foreach(Slot slot in OccupiedSlots)
        if (slot.CurrentItem.ID == item.ID)
        {
            slot.PushItem(item);
            return;
        }

    //Second Loop finds an unoccupied slot
    foreach (Slot slot in Slots)
        if (!slot.IsOccupied)
        {
            EmptySlots--;
            slot.PushItem(item);
            return;
        }
}

public static Item DropSelectedItem() => CurrentSelectedSlot.PopItem();

//Push all stacks in slots to front of list
public static void Reorganize()
{
    for(int i = 0; i < Slots.Count - 1;)
    {
        //Current slot is empty and next slot has stack, push stack back one index
        //Keep pushing Slots back until they all Slots are pushed to front of list
        if (!Slots[i].IsOccupied & Slots[i + 1].IsOccupied)
        {
            SwapSlotItems(i, i+1);
            
            //Avoid out of range
            if(i > 0) i--; 
            continue;
        }
        i++;
    }
}

//A/B Swap Function
public static void SwapSlotItems(int a, int b)
{
    //Temp stack reference
    Stack<Item> items = Slots[a].Items;

    Slots[a].Items = Slots[b].Items;
    Slots[b].Items = items;

    ChangeSprite(a);
    ChangeSprite(b);
}

#endregion
The Slot Elements inherit from the UISprite class I built, which implements from UIElement. Some basic Utility functions control adding and removing items from the Item stack
Slot.cs Snippet
#region Add and Remove Items

//Throws set number of stacks out
internal void RemoveItems(int quantity)
{
    for(int i = 0; i < quantity; i++) PopItem();
}

//Get an item from Item Stack
internal Item PopItem()
{
    if (!IsOccupied)    //If stack is empty before popping, return nothing
        return null;

    Item pop = items.Pop(); //Get the next item in stack

    if (!IsOccupied)    //If stack is empty after popping an item
    {
        ChangeSprite();    //Change UISprite back to default empty slot
        InventoryManager.EmptySlots++;     //Give a Slot back to inventory
    }
    UpdateText();
    return pop; 
}

//Add an item into item stack
internal void PushItem(Item item)
{
    ChangeSprite(item.UIFrameX, item.UIFrameY);    //If slot is empty, set to item's UI Sprite
    items.Push(item);   //Add item to slot
    UpdateText();
}
#endregion

#region Font Handling

internal void UpdateText()
{
    if (Count <= 1)
        uiCount.Text = "";
    else
        uiCount.Text = Count.ToString();
}

#endregion
                               
One of the main components I wanted to focus on early was developing a clean UI Handling System, that allowed a "fire and forget" approach to managing them. To start, I built a hierarchy of classes, inheriting functionality for different purposes. I further built upon the features of the code base as I iterated on the Red Rock Farmer. After the game was in a good place, I untangled UI elements scattered throughout the project with a UIManager class, which handles drawing, updating, adding, and removing elements.
This is an example of a 1920 by 1080 game, where the default inventory anchor is set to 50% on the x axis and 80% on the y. regardless of resolution, the position stays the same.
By contrast, a square resolution. The UI is still at screen position (.5, .8), relative to the 1:1 screen ratio
Slot is a custom UI element that inherits from UISprite. UISprite is the Image handling class, inheriting from the base class UIElement, which doesn't handle any drawing, but is an abstract class that manages positioning in Screen and World Space. It allows UI to be placed in the world, or stay on screen and attached to the Camera.
This is an implementation of the UI sprites within the Inventory class. There are several ways to move the Slots via their rows and cols, including setting anchors using a percent value, positioning in pixels, and positioning in pixels relative to another object. This is still in development, this current code is going to be refactored into the UIElement class, to position any element by screen percent or relative to another object.
InventoryManager.cs Snippet
/// <summary>
/// Get individual slot X position in Screen Space
/// </summary>
/// <param name="col">col of slot in array</param>
/// <param name="screenSpaceAnchorX">X position of slot as percent (0.0 - 1.0)</param>
/// <returns>X Position in Screen space as pixels</returns>
public static int GetSlotPositionX(int col, float screenSpaceAnchorX = 0f)
{
    int screenSpaceX = (int)(Camera.ViewPortWidth * screenSpaceAnchorX);
    int offset = col * SlotWidth - (ColCount * SlotWidth) / 2;
    return screenSpaceX + offset;
}

/// <summary>
/// Set slot position relative to another UI Element or Screen Space position
/// </summary>
/// <param name="col">col of slot in array</param>
/// <param name="relativeTo">Offset relative to another Screen space position</param>
/// <returns>X Position in Screen space as pixels</returns>
public static int GetSlotRelativePositionX(int col, int relativeTo) => 
    GetSlotPositionX(col) + relativeTo;

/// <summary>
/// Set slot position relative to another Screen Space position, with margins between cols
/// </summary>
/// <param name="col">col of slot in array</param>
/// <param name="relativeTo">Offset relative to another Screen space position</param>
/// <param name="spaceBetweenCols">Offset relative to another Screen space position</param>
/// <returns>X Position in Screen space as pixels</returns>
public static int GetSlotRelativePositionX(int col, int relativeTo, int spaceBetweenCols)
{
    int relativeSlotPositionX = GetSlotRelativePositionX(col, relativeTo);
    int spacing = (spaceBetweenCols * (col % ColCount)) - spaceBetweenCols;

    return relativeSlotPositionX + spacing;
}

/// <summary>
/// Get individual Slot Y position in Screen Space
/// </summary>
/// param name="row">Row of slot in array</param>
/// <param name="screenSpaceAnchorY">Y position of slot as percent (0.0 - 1.0)</param>
/// <returns>Slot Y Position in Screen space as pixels</returns>
public static int GetSlotPositionY(int row, float screenSpaceAnchorY = 0f)
{
    int screenSpaceY = (int)(Camera.ViewPortHeight * screenSpaceAnchorY);
    int offset = row * SlotHeight - (RowCount * SlotHeight) / 2;

    return screenSpaceY + offset;
}

/// <summary>
/// Get Slot Y Position relative to another Screen Space Position
/// </summary>
/// <param name="row">Row of Slot in array</param>
/// <param name="relativeTo">Offset relative to another Screen Space Position</param>
/// <returns>X Position in Screen space as pixels</returns>
public static int GetSlotRelativePositionY(int row, int relativeTo) => GetSlotPositionY(row) + relativeTo;

/// <summary>
/// Get Slot Y Position relative to another Screen Space Position, with space between Rows
/// </summary>
/// <param name="row">col of slot in array</param>
/// <param name="relativeTo">Offset relative to another Screen space position</param>
/// <param name="spaceBetweenRows">Offset relative to another Screen space position</param>
/// <returns>X Position in Screen space as pixels</returns>
public static int GetSlotRelativePositionY(int row, int relativeTo, int spaceBetweenRows)
{
    int relativeSlotPositionY = GetSlotRelativePositionY(row, relativeTo);
    int spacing = (spaceBetweenRows * (row % RowCount)) - spaceBetweenRows;

    return relativeSlotPositionY + spacing;
}

//Assign slot positions based on anchor points
public static void SetSlotPosition(int col, int row, 
float screenSpaceAnchorX = 0f, float screenSpaceAnchorY = 0f)
{
    Slot currentSlot = GetSlot(row * ColCount + col);   //Get slot out of list

    int x = GetSlotPositionX(col, screenSpaceAnchorX);
    int y = GetSlotPositionY(row, screenSpaceAnchorY);

    currentSlot.SetPosition(x, y);
}

//Assign Slot Position using anchor point (center position)
public static void SetSlotRelativePosition(int col, int row, 
int relativeX, int relativeY)
{
    Slot currentSlot = GetSlot(row * ColCount + col);   //Get slot out of list

    int x = GetSlotRelativePositionX(col, relativeX);
    int y = GetSlotRelativePositionY(row, relativeY);

    currentSlot.SetPosition(x, y);
}

//Assign Slot Position relative to a Screen Space Position
public static void SetSlotRelativePosition(int col, int row, 
int relativeX, int relativeY, int spaceBetweenCols, int spaceBetweenRows)
{
    Slot currentSlot = GetSlot(row * ColCount + col);   //Get slot out of list
    int x = GetSlotRelativePositionX(col, relativeX, spaceBetweenCols);
    int y = GetSlotRelativePositionY(row, relativeY, spaceBetweenRows);

    currentSlot.SetPosition(x, y);
}

public static void SetInventoryLayout(int rows, int cols,
float screenSpaceAnchorX, float screenSpaceAnchorY)
{
    SetSlotGrid(rows, cols);

    for(int i = 0; i < Slots.Count; i++)
    {
        int row = i / cols, 
            col = i % cols;
        SetSlotPosition(col, row, screenSpaceAnchorX, screenSpaceAnchorY);
    }
}

public static void SetInventoryLayout(int rows, int cols, 
int relativeX, int relativeY, int spaceBetweenCols = 0, int spaceBetweenRows= 0)
{
    SetSlotGrid(rows, cols);

    for (int i = 0; i < Slots.Count; i++)
    {
        int row = i / cols,
            col = i % cols;

        SetSlotRelativePosition(col, row, relativeX, relativeY, spaceBetweenCols, spaceBetweenRows);
    }
}
                                              
I didn't originally have a goal of building a level editor in MonoGame, but after making a few iterations, using a 2D array with Enums to label tiletypes, determining collision via child Tile classes, etc, it eventually made sense to make a 2D engine using window forms. The Level Editor requires 2 VS projects; LevelEditor and TileEngine.
Here is an eagle view of the Editor's Interface:
The Level Editor
  • The Level Editor is the series of tools that allow using the Tile Engine to "paint" different tiles onto a Map.
  • The interface is one part Window Form(s) and one part MonoGame executable.
    • Window forms hold all the user controls, sprite selection, imported spritesheets, etc.
    • The Tile Engine itself is what makes use of the Editor tools (it handles input), but the Map Editor Window Form takes care of setting tiles, which is sent to the Tile Engine to edit the tiles.
  • User Settings are also stored after most actions, including importing sprite sheets, adding new Code Values to mark tiles, etc.
  • Most of these actions, saving and loading, importing, are done through additional window forms and dialog windows.
Most of the Map Editor tools are in the MapEditor Window Form class. This is where I could pick which sprite to use, add spritesheets, decide whether a tile requires a CodeValue or Collision on a Tile, etc
MapEditor.cs Snippet
internal void AddSpriteSheet(string fileName, int animationFrames)
{
    if (!listSpriteSheet.Items.Contains(fileName))  //Sprite is not loaded in already
    {
        string filePath = Application.StartupPath + @"\Content\Textures\" + fileName;
        Bitmap newSpriteSheet = new Bitmap(filePath);
        int spriteSheetIndex = spriteSheets.Count;  //SpriteSheet number

        spriteSheets.Add(newSpriteSheet);   
        spriteFrames.Add(animationFrames);  //If SpriteSheet is an animated strip

        listSpriteSheet.Items.Add(fileName);    //Add SpriteSheet to List

        Settings.AddSpriteSheet(fileName, animationFrames); //Add SpriteSheet for Saving in User Settings
    }
}

void LoadImageList()
{
    imgListTiles.Images.Clear();
    listTiles.Items.Clear();
    Bitmap loadedSpriteSheet = spriteSheets[CurrentSpriteSheet];

    int tileCount = 0;

    for (int i = 0; i < (loadedSpriteSheet.Width / TileMap.TileWidth) / spriteFrames[CurrentSpriteSheet]; i++)
        for (int j = 0; j < loadedSpriteSheet.Height / TileMap.TileHeight; j++)
        {
            //Cut a tile out of larger bitmap
            Bitmap newBitmap = loadedSpriteSheet.Clone(
                new System.Drawing.Rectangle(
                    i * TileMap.TileWidth, j * TileMap.TileHeight,
                    TileMap.TileWidth, TileMap.TileHeight), System.Drawing.Imaging.PixelFormat.DontCare);

            imgListTiles.Images.Add(newBitmap); //Add to list of images

            string itemName = "";

            if (tileCount == 0)
                itemName = "Empty";

            //Add a new listview item
            listTiles.Items.Add(new ListViewItem(itemName, tileCount++));
        }
}
I implemented an Auto-save function that saves when the application exits. This saves Code Values and SpriteSheets that get added. They are then loaded in a new instance in the MapEditor's constructor
#region Save And Load User Settings

public void SaveSettings()
{
    BinaryFormatter formatter = new BinaryFormatter();
    FileStream fileStream = new FileStream(Application.StartupPath + @"\Settings\Settings.txt", FileMode.OpenOrCreate);
    formatter.Serialize(fileStream, Settings);
    fileStream.Close();
}

public void LoadSettings()
{
    if (File.Exists(Application.StartupPath + @"\Settings\Settings.txt"))
    {
        BinaryFormatter formatter = new BinaryFormatter();
        FileStream fileStream = new FileStream(Application.StartupPath + @"\Settings\Settings.txt", FileMode.Open);
        Settings = (Settings)formatter.Deserialize(fileStream);
        RestoreEditorSettings();    //Add loaded settings to Editor Window Form
        fileStream.Close();
    }
    else
        AddSpriteSheet("TerrainSpriteSheet.png", 1);    //If load fails/ no file located in folder, reinitialize with default sprite sheet
}

void RestoreEditorSettings()
{
    //Add all the saved user code values to combo box
    for (int i = 0; i < Settings.cboCodeValues.Count; i++)
        cboCodeValues.Items.Add(Settings.cboCodeValues[i]);

    itemCodeValues.AddRange(Settings.gameCodeValues); //Make all code values available to TileEngine

    //Add all loaded sprite sheets
    for (int i = 0; i < Settings.spriteSheets.Count; i++)
        AddSpriteSheet(Settings.spriteSheets[i], Settings.spriteSheetFrames[i]);
}

public void RestoreTileMapSettings()
{
    if (Settings.spriteSheets.Count > 0)
        for (int i = 0; i < Settings.spriteSheets.Count; i++)
            TileMap.AddSpriteSheet(Settings.spriteSheets[i].Replace(".png", ""));
    else
        TileMap.AddSpriteSheet("TerrainSpriteSheet");
}

#endregion
To import a SpriteSheet, a dialog box is opened, specifying the usable file types and adding them to the MapEditor
NewSpriteSheetForm.cs Snippet
void browseButton_Click(object sender, EventArgs e)
{
    using (openFileDialog)
    {
        openFileDialog.InitialDirectory = "c:\\";   //Starting directory
        
        //Acceptable File Types
        openFileDialog.Filter = "Image Files (*.png, *.jpg) | *.jpg; *.png | All Files (*.*) | *.*";
        openFileDialog.FilterIndex = 2;
        openFileDialog.RestoreDirectory = true;

        if(openFileDialog.ShowDialog() == DialogResult.OK)
        {
            //Assign found image to file
            File = Image.FromFile(openFileDialog.FileName);

            //Display in textBox
            filePath = openFileDialog.FileName;
            directoryPath = Path.GetDirectoryName(filePath);

            Stream fileStream = openFileDialog.OpenFile();

            using(StreamReader reader = new StreamReader(fileStream))
            {
                fileContent = reader.ReadToEnd();
            }
        }
    }
    txtBoxFilePath.Text = openFileDialog.SafeFileName;
}

private void btnImport_Click(object sender, EventArgs e)
{
    if(File != null && openFileDialog != null)
        File.Save(Application.StartupPath + "\\Content\\Textures\\" + openFileDialog.SafeFileName);

    editor.AddSpriteSheet(txtBoxFilePath.Text, (int)cboAnimationFrames.SelectedItem);
}
The Tile Engine
  • The Tile Engine is the bridge between Level Construction and the Projects that use it.
  • The Tile Engine handles all Saving and Loading the Map Files in both the Level Editor and any Projects that use the Map that were generated.
    • Maps are Saved as .MAP and sent to the Content folder for use.
    • They are also are Loaded by their File names.
  • Everything that is defined in the Map Editor gets funneled into Tiles on a TileMap class.
    • Through the use of overloaded SetTile( . . . ) methods, I can set update tile settings on the fly. The point of this is to change individual settings without creating an entirely new Tile class. Some Uses include
      • Update Collision settings
      • Change a Sprite on a particular layer
      • Set Tile Sprite from static to animated
      • Tag a tile using Code Value. This is to mark tiles for purposes in the projects. This is analagous to Layers in Unity, or something similar to Collision Channels on Actors in Unreal.
When using the TileMap for the MapEditor, the functionality it performs is primarily editing Tile Cells on a Map. The MapEditor.cs merely tells the TileMap what to set on a tile.
TileMap.cs Snippets
//Set a new Tile to cell at index
public static void SetTileAtCell(int tileX, int tileY, Tile tile)
{
    if (IsInMapBounds(tileX, tileY))
        mapCells[tileX, tileY] = tile;
}

//Overload --- Updates the Current Tile at index
static public void SetTileAtCell(int x, int y, int layer, int tileIndex,
int spriteSheetIndex, string animationKey = "", string animationMode = "", float frameTimer = 0.0f)
{
    if (!IsInMapBounds(x, y)) return;

    Tile tile = mapCells[x, y];

        tile.ClearAnimation(layer);         //Remove any animations on layer
        tile.LayerTiles[layer] = tileIndex; 
        tile.LayerSpriteSheetIndices[layer] = spriteSheetIndex;
        tile.AnimationKey[layer] = animationKey;    //Tagging for animation
        tile.AnimationPlayMode[layer] = animationMode;  //How animation plays back
        tile.FrameTimer = frameTimer;   //Frame speed
}

//Set collision for a tile by map location
static public void SetTileCollider(int x, int y, int boundX, int boundY, int width, int height)
{
    if (IsInMapBounds(x, y))
        SetTileCollider(ref mapCells[x, y], boundX, boundY, width, height);
}

//Set collision directly to Tile
static public void SetTileCollider(ref Tile tile, int boundX, int boundY, int width, int height)
{
    tile.boundX = boundX;
    tile.boundY = boundY;
    tile.boundWidth = width;
    tile.boundHeight = height;
}
Saving and Loading uses FileStreaming, which in a later iteration will be swapped with XML files, for more flexible Tile Engine version transitions when updates are made in the Engine Code.
//***********************************************//
#region Saving and Loading

//Serialize map to binary for saving
public static void Save(FileStream fileStream)
{
    //Resets all animations to frame zero when saving!
    foreach (Tile tile in mapCells)
        for (int i = 0; i < tile.LayerTiles.Length; i++)
            if (tile.LayersCurrentSprite[i].Count > 0)
            {
                tile.LayerTiles[i] = tile.LayersCurrentSprite[i][0];
            }

    BinaryFormatter formatter = new BinaryFormatter();
    formatter.Serialize(fileStream, mapCells);


    fileStream.Close();
}

//Deserialize map for loading
public static bool Load(FileStream fileStream)
{
    try
    {
        BinaryFormatter formatter = new BinaryFormatter();
        mapCells = (Tile[,])formatter.Deserialize(fileStream);

        fileStream.Close();
        Console.WriteLine(fileStream.ToString());

        return true;
    }
    catch
    {
        Console.WriteLine("No Map found for stream: " + fileStream.ToString());
        fileStream.Close();
        return false;
    }
}
#endregion
//***********************************************//
Red Rock Farmer was my first foray into Pixel Art Design. It was a lot of trial and error at first. In Photoshop, I developed a workflow of categorizing into layers: Solid Base Layer, Outline Layer, Shading, and Highlights. Not a complete distillation of my workflow, but that's been the base to start from with every sprite. Any sprite groups were packed into a single spritesheet.
Tile SpriteSheets
Each Tile is 48W * 48H dimensions. A tile is located in the Tile Engine by index % Cols and index / Cols
Width: 288px
Height: 240px
Row: ( i / Cols.Count ) * Width
Col: ( i % Cols.Count ) * Height
Sprite Count: Rows * Cols = 30 Sprites
Functionality for Spritesheets used with Tile Engine. Cutting frames is based on the Tile Dimensions. Base class Item is also a part of the Tile Engine class, which can be inherited in any project.
//***********************************************//
#region Tile Sheet Methods

public static void AddSpriteSheet(string fileName)
{
    if (!tileSheets.Contains(Content.Load<Texture2D>(@"Textures\" + fileName)))
        tileSheets.Add(Content.Load<Texture2D>(@"Textures\" + fileName));
}

//Number of Tiles in a Row of the tileSheet
public static int TilesPerCol(int spriteSheetIndex) => 
    tileSheets[spriteSheetIndex].Height / TileHeight;

//Get a tile from the tilesheet based on the index
public static Rectangle TileSourceRectangle(int tileIndex, int spriteSheetIndex)
{
    //Uses 2D-to-1D conversion to get a tile from the tileSheet
    return new Rectangle(
        (tileIndex / TilesPerCol(spriteSheetIndex)) * TileWidth,
        (tileIndex % TilesPerCol(spriteSheetIndex)) * TileHeight,
        TileWidth, TileHeight);
}
//Gets the items Sprite Sheet Location
public static Rectangle ItemSourceRectangle(Item item)
{
    return new Rectangle(
        item.FrameX * TileWidth, item.FrameY * TileHeight,
        item.FrameWidth, item.FrameHeight);
}
#endregion
//***********************************************//
TODO: Explain LoadImageList
void LoadImageList()
{
    imgListTiles.Images.Clear();
    listTiles.Items.Clear();
    Bitmap loadedSpriteSheet = spriteSheets[CurrentSpriteSheet];
 
    int tileCount = 0;
 
    for (int i = 0; i < (loadedSpriteSheet.Width / TileMap.TileWidth) / spriteFrames[CurrentSpriteSheet]; i++)
        for (int j = 0; j <= loadedSpriteSheet.Height / TileMap.TileHeight; j++)
        {
            //Cut a tile out of larger bitmap
            Bitmap newBitmap = loadedSpriteSheet.Clone(
                new System.Drawing.Rectangle(
                    i * TileMap.TileWidth, j * TileMap.TileHeight,
                    TileMap.TileWidth, TileMap.TileHeight), System.Drawing.Imaging.PixelFormat.DontCare);
 
            imgListTiles.Images.Add(newBitmap); //Add to list of images
 
            string itemName = "";
 
            if (tileCount == 0)
                itemName = "Empty";
 
            //Add a new listview item
            listTiles.Items.Add(new ListViewItem(itemName, tileCount++));
        }
}
Animated SpriteSheets
Animating the spritesheet is a similar practice to cutting sprites out of a spritesheet. The only difference is adding a timer and calculating the frame count to determine an animation's end. It works like this
  • A timer ticks every frame and asks if the animation should be updated.
  • Increment the Current Frame and check if it is at the end of animation.
    • If it is a looping animation, start back at frame 0.
    • Else make sure the Current Frame ends on the last frame (FrameCount - 1) and stop playing.
  • The FrameCount is a property that calculates Spritesheet Width / Frame Width.
  • The Current Frame is used to slice a rectangle frame in another Animation property.
    • An example of the frame rectangle is demonstrated below
FrameWidth: 96px
FrameHeight: 96px
X : FrameWidth * =
Y : 0
Current Frame:
Y:0
X:
96H
96W
The snippet of code that runs an animation is essentially the code implementation of the above illustration.
Animation.cs Snippet
internal int FrameCount => texture.Width / frameWidth;
//Get current frame of animation
internal Rectangle FrameRectangle =>
    new Rectangle(
        CurrentFrame * frameWidth, 0, 
        frameWidth, frameHeight);

#region Constructor
public Animation(Texture2D texture, int frameWidth, string
 name)
{
    this.texture = texture;
    this.frameWidth = frameWidth;
    this.frameHeight = texture.Height;
    this.name = name;
}
#endregion

#region Methods
internal void BeginPlay()
{
    CurrentFrame = 0;
    isPlaying = true;
}

//Called when isPlaying
internal void Play(GameTime gameTime)
{
    frameTick += (float)gameTime.ElapsedGameTime.TotalSeconds;

    if (frameTick < frameDelay) return;

    frameTick = 0f;
    CurrentFrame++;

    //End of animation
    if (CurrentFrame >= FrameCount)
    {
        if(loopingAnimation)
            CurrentFrame = 0;   //Reset Animation
        else
        {
            CurrentFrame = FrameCount - 1;  //End of Animation
            isPlaying = false; //Stop playing animation
        }
    }
}
#endregion
Sprite Objects have animation functionality at the GameObject class level. A dictionary holds multiple animations and are played by look-ups. If needed, there is a linked-list that attaches a Next Animation to play when the current animation is complete.
GameObject.cs Snippet
//Checks Current Animation Status and Updates if Required
void UpdateAnimation(GameTime gameTime)
{
    if (animations.ContainsKey(currentAnimation))   //Check in LookUpTable for anim
        //Check if animation status is finished
        if (!animations[currentAnimation].IsPlaying)
        {
            //Get next animation if applicable
            PlayAnimation(animations[currentAnimation].NextAnimation);
        }
        else
            animations[currentAnimation].Play(gameTime);  //Continue playing Animation
}

//Checks if animation exists in Lookup Table and Plays if found
internal void PlayAnimation(string name)
{
    if (name != null && animations.ContainsKey(name))
    {
        currentAnimation = name;
        animations[name].BeginPlay();
    }
}