Showing posts with label Code. Show all posts
Showing posts with label Code. Show all posts

Wednesday, November 27, 2024

Combat Scene Update

 Lots of progress has been made on multiple fronts.

The combat Scene now properly loads a level(different from a map) and tracks mouse movement. It also Is able to load dialogue once specified conditions are met. I've also got the scaffolding for Adding camera controls and overlays to highlight parts of the map, though it isn't quite presentable.



Here we see a sample dialogue from dialogue manager playing once the map is loaded

Then general workflow has the battle load up everything. Data specific to the scenario is pulled from a scenario file and stored in a map node, which has wrapped code to access it. The debug console does not like accessing instantiated nodes, so this allows me to access the map indirectly. I'll likely need to do something similar for when I add units.

The scenario file is a node, so I'll be relying on virtual functions to add in dialogues, victory conditions, and anything else a scripted battle may need.


I've also developed a couple of tools to assist in making sprites. Character sprites(I'm calling them actors as they will also control movement and actions) will be one of two types: component actors and sprite actors.

Sprite actors will be straight forward sprites. They are a single loaded texture that can play animations. ComponentActors are made of multiple sprites in order to facilitate customization. Change armor? The sprite will reflect that.

I am using the Time elements Character Core Set and the expansion, both by FinalBossBlues. The pack does come with a character generator that can generate spritesheets for you, but that would mean I would have to prebake every sprite.

What I've done instead is create the Sprite Slicer too. 


It is easy to use. I'm using some of the frame lists that FinalBossBlues uses for his animations. So the walk cycle will look the same as the pre generated one. If I wanted to change a frame, I just need to change the array entry.

Once The animations are done, I then run the script, which will generate each animation in all 4 direction and store them in an animation library. I can then load that animation library into the Actor class and it will have access to all the animations. The actor also already has a premade animation tree to handle transitions.


    public void CreateAnimation(Array<int> frames, int direction, string name, bool loop)
    {       
        Animation animation = new Animation();
        int ind = animation.AddTrack(Animation.TrackType.Value);
        animation.TrackSetPath(ind, ".:frame_coords");
        animation.ValueTrackSetUpdateMode(ind, Animation.UpdateMode.Discrete);
        if (loop)
            animation.LoopMode = Animation.LoopModeEnum.Linear;
        animation.Length = (frames.Count) * speed;
        for (int i = 0; i < frames.Count; i++)
        {


            Vector2I frame = new Vector2I(frames[i], direction);
            GD.Print("frame " + frame);
            int index = animation.TrackInsertKey(ind, speed * i, new Vector2(frames[i], direction));

        }

        animationLibrary.AddAnimation($"{name}_{IntToDir(direction)}", animation);

    }

This is the core logic. It creates the empty animation and creates a track for the sprite sheet.. It then adds each frame, spaced out by a speed variable defined earlier in the code. This means all animations will run at the same rate.

Finally, it adds the animation to the library created previously. 

The spritesheets provided by FinalBossBlues each have 4 rows of animations, each one representing the sprite in one direction, so it is easy to just run the code 4 times, once for each row.

I purchased these assets, but I'm not sure if I am allowed to post one as an example.

I've also mostly finished a tool to use these spritesheets to generate sprites.



This one runs in engine and allows me to see a sprite with different pieces from different directions. It does still need a way to actually export the animation and preview animations, but the core of it is working. 


This code feels a bit messy, but here is the breakdown

    [Export]
    string AssetsFolder = "Folder containing spritesheets goesHere";
    [Export]
    Array<string> Components = ["Each subfolder goes here];

This shows the script where to find the spritesheets.

System.Collections.Generic.Dictionary<string, System.Collections.Generic.Dictionary<int, List<Component>>> Folders = [];

This is a bit of a monster of a datastructure definition.

The Inner Dictionary correlates Each Sprite type with the different colors it has available. For example, The first type of of hair contains hair1.png, but it also has color variations hair1_c1.png, hair1_c2.png, etc.

The outer dictionary then stores all of the sprite types with its category(hair, weapon, etc.).


After some boiler plate, we then make a spinner setup(I called it a SpriteMakerComponentPicker) for each category. It updates the texture when either picker is changed.

Some components have a 0 item index (bottom0.png), so instead of getting the spritesheet by the value of the spinner directly, I have to use spinner's value to retrieve the key that we need.

componentPicker.PartUpdated += (value) =>
            {
                if (value == -1)
                {
                    CustomizedActor.SetTexture(comp, null);
                    return;
                }
                int color = componentPicker.GetColorValue();
                int key = ComponentListByIDDictionary.Keys.ToArray()[value];
                List<Component> components = ComponentListByIDDictionary[key];
                Component component = components[componentPicker.GetColorValue()];
                Texture2D tex = GetTexture(comp, componentPicker.GetIDValue(), component);
                CustomizedActor.SetTexture(comp, tex);
            };
            componentPicker.ColorUpdated += (value) =>
            {
                if (value == -1)
                {
                    CustomizedActor.SetTexture(comp, null);
                    return;
                }
                int color = componentPicker.GetColorValue();
                List<Component> components = ComponentListByIDDictionary[value];
                int key = ComponentListByIDDictionary.Keys.ToArray()[value];
                Component component = components[componentPicker.GetColorValue()];
                Texture2D tex = GetTexture(comp, componentPicker.GetIDValue(), component);
                CustomizedActor.SetTexture(comp, tex);
            };

At this point, I can likely combine the events into one.


public struct Component
    {
        public string category;
        public int id;
        public int color;
    }

Component is a basic data structure used to help find the spritesheet's filename. everything follows the format

    categoryX.png or categoryX_cY.png, 

except weapons, which swap category with a weapon type,

I've left a lot of code out, but if you are interested in seeing a more complete version let me know.



Currently I have the following short term goals:

  1. Make the SpriteMakerTool Export to file and get loaded by the Actor class
  2. Give functionality to the actor class. Perhaps I'll go back to calling it a sprite and abstract movement code.




Saturday, November 23, 2024

Level Scene


 In order to make a tactical rpg, there needs to be a grid. Godot has a built in tile editor. For the purposes of testing(and maybe published, who knows) I'll be using the Grasslands 4 Seasons 16x16 Tileset by ElvGames. I purchased it as part of a humble bundle pack, but you can purchase it here


I've used this tileset to create a random setup that will work for our purposes. Just a random field.




Two of the tileset atlases

Another one
Some objects that are availible
The result

Each Level will also have a collision layer deciding whether or not a tile is walkable

There is nothing super complicated in the code. I did get some of the formulas from  GDQuests tactical movement series found here. The GitHub is found here I personally prefer c#, so some conversion and tweaking was done. The code itself is released under the MIT License


The ready function makes sure to hide the collision layer unless that feature is disabled.

    public override void _Ready()
    {
        base._Ready();
        if (HideCollision)
        {
            Visible = false;
        }
    }

This is for when a tile is represented as a object in a 1d array(or needs to be

    //Converts a 2d coordinate to an array index
    public int ToIndex(Vector2I tileCoordinate)
    {
        return (int)(tileCoordinate.X + GetMapSize().X * tileCoordinate.Y);
    }
    //converts an array index to a 2d coordinate
    public Vector2I IndexToCell(int index)
    {
        int mapWidth = GetMapSize().X;
        int x = index % mapWidth;
        int y = index / mapWidth;
        return new Vector2I(x, y);

    }

Boundary checking code is as follows:

     public bool IsWalkableTile(Vector2I position)
    {
        return CollisionLayer.GetCellSourceId(position) != -1;
    }

    public Vector2I ClampTile(Vector2I tileCoordinate)
    {
        int X = tileCoordinate.X;
        int Y = tileCoordinate.Y;
        Vector2I mapSize = GetMapSize();
        if (X < 0)
            X = 0;
        if (X > mapSize.X)
            X = mapSize.X;
        if (Y < 0)
            Y = 0;
        if (Y > mapSize.Y)
            Y = mapSize.Y;
        return new Vector2I(X, Y);
    }

    public bool IsInBounds(Vector2I tileCoordinate)
    {
        int X = tileCoordinate.X;
        int Y = tileCoordinate.Y;
        Vector2I mapSize = GetMapSize();
        if (X < 0 || Y < 0)
            return false;
        if (X > mapSize.X || Y > mapSize.Y)
            return false;
        return true;
    }

Getting the coordinates of a tile and the tile at a set of coordinates is also important

    public Vector2I PixelToTile(Vector2I pixelCoordinates)
    {
        return (Vector2I)new Vector2(Mathf.Floor(pixelCoordinates.X / GetResolution()), Mathf.Floor(pixelCoordinates.Y / GetResolution()));
    }

    //Returns the pixel coordinates of the tile based off the center of the tile
    public Vector2I TileToPixel(Vector2I TileCoordinates)
    {
        Vector2I halfSize = new Vector2I(GetResolution() / 2, GetResolution() / 2);
        return TileCoordinates * GetResolution() + halfSize;
    }



I also have not tested this code out yet, but beyond typos, the logic should be good.

Oh, I also installed a developer console addon. I think I'm addicted to plug ins.







This is the Developer Console for AssetLib by HamsterByte. You can find it here

It is licensed under MIT. I have not done anything with it yet, but it surely will be good for testing. Right?


Below is the majority of the file as I have it right now. I'll eventually be using a github(why am I not already!? Seriously) but this will do for now.

namespace Data.Enum;

public enum LevelResolution
{
    Undefined = 0,
    R16x16 = 16,
    R32x32 = 32,
    R48x48 = 48
}


namespace Data.Enum;
[GlobalClass]
public partial class Level : Node2D
{
    [Export]
    LevelResolution resolution;
    [Export]
    bool HideCollision = false;
    TileMapLayer CollisionLayer { get { return (TileMapLayer)FindChild("Collision"); } }
    Array<TileMapLayer> Layers
    {
        get
        {
            Array<TileMapLayer> layers = new Array<TileMapLayer>();
            Array<Node> children = GetChildren();
            foreach (Node item in children)
            {
                if (item.GetType() != typeof(TileMapLayer))
                    continue;
                if (item == CollisionLayer)
                    continue;

                layers.Add((TileMapLayer)item);
            }
            return layers;
        }
    }

    #region ConsoleCommands

    [ConsoleCommand]
    public int GetResolution()
    {
        return (int)resolution;
    }
    [ConsoleCommand]
    public Vector2I GetMapSize()
    {
        Vector2I foundSize = Vector2I.Zero;

        Array<TileMapLayer> layers = Layers;
        foreach (TileMapLayer layer in layers)
        {
            Rect2I rect = layer.GetUsedRect();
            if (rect.Size.X > foundSize.X)
                foundSize = new Vector2I(rect.Size.X, foundSize.Y);
            if (rect.Size.Y > foundSize.Y)
                foundSize = new Vector2I(foundSize.X, rect.Size.Y);
        }
        return foundSize;
    }
    [ConsoleCommand]
    public Vector2I GetPixelMapSize()
    {
        Vector2I mapSize = GetMapSize();
        return mapSize * GetResolution();
    }

    [ConsoleCommand]
    public Vector2I MouseToTile()
    {
        Vector2I mousepos = (Vector2I)GetLocalMousePosition();
        return PixelToTile(mousepos);
    }

    public Vector2I MouseToPixel()
    {
        Vector2I mousepos = (Vector2I)GetLocalMousePosition();
        Vector2I mouseTile = PixelToTile(mousepos);
        Vector2I tilePixel = TileToPixel(mouseTile);
        return tilePixel;

    }
    [ConsoleCommand]
    public Vector2I PixelToTile(int pixelX, int pixelY)
    {
        return PixelToTile(new Vector2I(pixelX, pixelY));
    }

    [ConsoleCommand]
    public Vector2I PixelToTile(Vector2I pixelCoordinates)
    {
        return (Vector2I)new Vector2(Mathf.Floor(pixelCoordinates.X / GetResolution()), Mathf.Floor(pixelCoordinates.Y / GetResolution()));
    }

    [ConsoleCommand]
    public Vector2I TileToPixel(int TileX, int TileY)
    {

        return TileToPixel(TileX, TileY);
    }

    [ConsoleCommand]
    public bool IsWalkableTile(int TileX, int TileY)
    {
        return IsWalkableTile(new Vector2I(TileX, TileY));
    }
    [ConsoleCommand]
    public Vector2I ClampTile(int TileX, int TileY)
    {
        return ClampTile(new Vector2I(TileX, TileY));
    }
    [ConsoleCommand]
    public bool IsInBounds(int TileX, int TileY)
    {
        return IsInBounds(new Vector2I(TileX, TileY));
    }
    #endregion
    //Returns the pixel coordinates of the tile based off the center of the tile
    public Vector2I TileToPixel(Vector2I TileCoordinates)
    {
        Vector2I halfSize = new Vector2I(GetResolution() / 2, GetResolution() / 2);
        return TileCoordinates * GetResolution() + halfSize;
    }

    public bool IsWalkableTile(Vector2I position)
    {
        return CollisionLayer.GetCellSourceId(position) != -1;
    }

    public Vector2I ClampTile(Vector2I tileCoordinate)
    {
        int X = tileCoordinate.X;
        int Y = tileCoordinate.Y;
        Vector2I mapSize = GetMapSize();
        if (X < 0)
            X = 0;
        if (X > mapSize.X)
            X = mapSize.X;
        if (Y < 0)
            Y = 0;
        if (Y > mapSize.Y)
            Y = mapSize.Y;
        return new Vector2I(X, Y);
    }

    public bool IsInBounds(Vector2I tileCoordinate)
    {
        int X = tileCoordinate.X;
        int Y = tileCoordinate.Y;
        Vector2I mapSize = GetMapSize();
        if (X < 0 || Y < 0)
            return false;
        if (X > mapSize.X || Y > mapSize.Y)
            return false;
        return true;
    }

    //Converts a 2d coordinate to an array index
    public int ToIndex(Vector2I tileCoordinate)
    {
        return (int)(tileCoordinate.X + GetMapSize().X * tileCoordinate.Y);
    }
    //converts an array index to a 2d coordinate
    public Vector2I IndexToCell(int index)
    {
        int mapWidth = GetMapSize().X;
        int x = index % mapWidth;
        int y = index / mapWidth;
        return new Vector2I(x, y);

    }
    public override void _Ready()
    {
        base._Ready();
        if (HideCollision)
        {
            Visible = false;
        }
    }


}

Some methods are overloaded to allow versions that work with console commands

I'll be leaving out includes and such, but it should be easy to get this code to work in your own project.