ecsx-framework / ECSx

An Entity-Component-System framework for Elixir
GNU General Public License v3.0
210 stars 10 forks source link

Investigate game map/environment seeding #18

Closed APB9785 closed 8 months ago

APB9785 commented 1 year ago

We want to find a convenient way for the game developer to design layouts for game levels including pieces of environment, boundaries, spawn points, etc.

These could be either publicly exposed for use in the initial setup block, or automatically set up by the library somehow.

artokun commented 1 year ago

Hey there! I wanted to suggest using a 3D tilemap system for your game project. In a 3D tilemap system, this grid is represented in three dimensions, using X and Y coordinates to define the position of the tiles and a Z axis to organize layers. I've found that this approach really helps with organizing and optimizing game environments. Let me give you a quick rundown of how you might set it up:

Layer 1: Start with a base layer for background terrain like grass or dirt or in the case for this game water. It's non-interactive and lays the foundation for the game world. Layer 2: Add some visual depth with decorative elements such as shrubs or rocks. They're non-interactive too, but they'll make your game look more polished. Layer 3: Interactive objects like floatsam or buoys. These will give your players something to engage with. Layer 4: Set up boundaries and navigation blockers like walls or cliffs. You could also include ramps or high-cost traversal tiles for more interesting character movement that affects your nav mesh with using A* or some flavor of Dykstra's nav algorithm. Layers 5-6: Allocate these for characters, including player-controlled ones and enemies. This makes managing their interactions a lot easier. Layers 7-n: things like health bars, birds, overlays, lighting, effects, etc.

By using this layered approach, you'll find it much easier to handle rendering and organization. Plus, you can always mask layers globally for different purposes, like hiding/revealing game elements when needed.

Give it a shot! I think you'll be happy with the results. Let me know if you have any questions or need further clarification. Good luck!

artokun commented 1 year ago

if you're interested in the differences in orthogonal and hexagonal grids I thing this guide to be fantastic https://www.redblobgames.com/grids/hexagons/

APB9785 commented 1 year ago

@artokun Thanks for all your input! I will definitely keep all this info in mind going forwards as I continue to build out the demo game and my own personal games.

In this specific issue, the challenge I'm trying to tackle is what data structure should be used for the map file. So far I've considered two main options:

  1. An "ASCII art" type text file, where each symbol represents a different type of tile, to be parsed by the game backend upon startup. This would allow a developer to visually design a 2D map, and conveniently add or edit it later, without the need for a GUI.
  2. A list of coordinates, with info about each, to be parsed by the game backend upon startup. This would allow much richer information to be stored, but would make it very difficult for a developer to design or edit without a GUI.

Since there is currently no GUI for map design/editing in this library (yet), it seems we must look towards option 1, at least temporarily. But I'm still interested in finding additional possibilities.

artokun commented 1 year ago

I think I have a solution that can combine the best of both worlds. Instead of choosing between an ASCII art type text file and a list of coordinates, you can use a hybrid approach.

You can use a simple file format that includes both human-readable ASCII art and additional metadata for richer information. The idea is to use a plain text file that consists of a 2D grid with ASCII characters for the visual layout and a separate section for extra data related to each tile.

Here's an example of how the file could be structured:

[Layout]
#####
#...#
#.@.#
#...#
#####

[Metadata]
.;type=grass;walkable=true
@;type=spawn_point;player=1
#;type=wall;walkable=false

In this example, the [Layout] section contains the ASCII art representation of the map, while the [Metadata] section provides additional information about each tile type. The format is flexible and can be easily expanded to include more details about tiles or objects on the map.

This way, you'll have the convenience of designing and editing 2D maps visually (like in option 1) while also having the ability to store rich metadata about each tile (like in option 2). You can then parse the file upon startup and create the appropriate data structures for your game engine.

Of course, when you eventually create a GUI for map design and editing, you can move towards a more sophisticated file format that stores all necessary information, but this hybrid approach should serve as a useful starting point.

However, for tutorial purposes, this format could be used in both terminal and web as it's a simple prototypal way to quickly build maps with just ASCII!

Here's an example MapParser

defmodule MapParser do
  @moduledoc """
  A module for parsing map files containing layout and metadata sections.
  """

  @doc """
  A struct representing a parsed map file, containing layout and metadata.
  """
  defstruct layout: %{}, metadata: %{}

  @doc """
  Parses a map file and returns a %MapParser{} struct.
  """
  @spec parse_file(String.t()) :: %MapParser{}
  def parse_file(file_path) do
    {:ok, content} = File.read(file_path)
    parse(content)
  end

  @doc """
  Parses a map file content and returns a %MapParser{} struct.
  """
  @spec parse(String.t()) :: %MapParser{}
  def parse(content) do
    sections = String.split(content, ~r/\n\n/, trim: true)
    layout = parse_layout(Enum.at(sections, 0))
    metadata = parse_metadata(Enum.at(sections, 1))

    %MapParser{layout: layout, metadata: metadata}
  end

  @doc """
  Parses the layout section of the map file and returns a list of lists of strings.
  """
  @spec parse_layout(String.t()) :: list(list(String.t()))
  defp parse_layout(layout_section) do
    layout_lines = String.split(layout_section, "\n", trim: true)
    Enum.map(layout_lines, &String.graphemes(&1))
  end

  @doc """
  Parses the metadata section of the map file and returns a map of symbols to their metadata.
  """
  @spec parse_metadata(String.t()) :: map()
  defp parse_metadata(metadata_section) do
    metadata_lines = String.split(metadata_section, "\n", trim: true)

    Enum.reduce(metadata_lines, %{}, fn line, acc ->
      [symbol, metadata] = String.split(line, ";", trim: true)
      tile_metadata = parse_tile_metadata(metadata)
      Map.put(acc, symbol, tile_metadata)
    end)
  end

  @doc """
  Parses a metadata string and returns a map of key-value pairs.
  """
  @spec parse_tile_metadata(String.t()) :: map()
  defp parse_tile_metadata(metadata) do
    Enum.reduce(String.split(metadata, ";"), %{}, fn item, acc ->
      [key, value] = String.split(item, "=", trim: true)
      Map.put(acc, String.to_atom(key), value)
    end)
  end
end

You can use the parse_file/1 function to parse a map file and create a %MapParser{} struct containing the layout and metadata.

Here's an example of how to use this module:

map_file = "path/to/your/map_file.txt"
parsed_map = MapParser.parse_file(map_file)

IO.inspect(parsed_map.layout)     # This will print the parsed layout
IO.inspect(parsed_map.metadata)   # This will print the parsed metadata

Keep in mind that this implementation assumes a valid and well-formatted map file. You might want to add error handling and validations to make it more robust.

Let me know if you have any questions or areas you'd like to discuss, or if you prefer an even more robust solution so you can use something like Tiled where you design your map and export a JSON object that you'd then parse into the game.