Git Product home page Git Product logo

concord's Introduction

Concord

Concord is a feature complete ECS for LÖVE. It's main focus is performance and ease of use. With Concord it is possibile to easily write fast and clean code.

This readme will explain how to use Concord.

Additionally all of Concord is documented using the LDoc format. Auto generated docs for Concord can be found in docs folder, or on the Github page.


Table of Contents

Installation
ECS
API :

Quick Example
Contributors
License


Installation

Download the repository and copy the 'src' folder in your project. Rename it to something that makes sense (Probably 'concord'), then require it in your project like so:

local Concord = require("path.to.concord")

Concord has a bunch of modules. These can be accessed through Concord:

-- Modules
local Entity     = Concord.entity
local Component  = Concord.component
local System     = Concord.system
local World      = Concord.world
local Assemblage = Concord.assemblage

-- Containers
local Components  = Concord.components
local Systems     = Concord.systems
local Worlds      = Concord.worlds
local Assemblages = Concord.assemblages

ECS

Concord is an Entity Component System (ECS for short) library. This is a coding paradigm where composition is used over inheritance. Because of this it is easier to write more modular code. It often allowes you to combine any form of behaviour for the objects in your game (Entities).

As the name might suggest, ECS consists of 3 core things: Entities, Components, and Systems. A proper understanding of these is required to use Concord effectively. We'll start with the simplest one.

Components

Components are pure raw data. In Concord this is just a table with some fields. A position component might look like { x = 100, y = 50}, whereas a health Component might look like { currentHealth = 10, maxHealth = 100 }. What is most important is that Components are data and nothing more. They have 0 functionality.

Entities

Entities are the actual objects in your game. Like a player, an enemy, a crate, or a bullet. Every Entity has it's own set of Components, with their own values.

A crate might have the following components:

{
    position = { x = 100, y = 200 },
    texture  = { path = "crate.png", image = Image },
    pushable = { },
}

Whereas a player might have the following components:

{
    position     = { x = 200, y = 300 },
    texture      = { path = "player.png", image = Image },
    controllable = { keys = "wasd" },
    health       = { currentHealth = 10, maxHealth = 100},
}

Any Component can be given to any Entity (once). Which Components an Entity has will determine how it behaves. This is done through the last thing...

Systems

Systems are the things that actually do stuff. They contain all your fancy algorithms and cool game logic. Each System will do one specific task like say, drawing Entities. For this they will only act on Entities that have the Components needed for this: position and texture. All other Components are irrelevant.

In Concord this is done something alike this:

drawSystem = System({position, texture}) -- Define a System that takes all Entities with a position and texture Component

function drawSystem:draw() -- Give it a draw function
    for _, entity in ipairs(self.pool) do -- Iterate over all Entities that this System acts on
        local position = entity[position] -- Get the position Component of this Entity
        local texture = entity[texture] -- Get the texture Component of this Entity

        -- Draw the Entity
        love.graphics.draw(texture.image, position.x, position.y)
    end
end

To summarize...

  • Components contain only data.
  • Entities contain any set of Components.
  • Systems act on Entities that have a required set of Components.

By creating Components and Systems you create modular behaviour that can apply to any Entity. What if we took our crate from before and gave it the controllable Component? It would respond to our user input of course.

Or what if the enemy shot bullets with a health Component? It would create bullets that we'd be able to destroy by shooting them.

And all that without writing a single extra line of code. Just reusing code that already existed and is guaranteed to be reuseable.


API

General design

Concord does a few things that might not be immediately clear. This segment should help understanding.

Classes

When you define a Component or System you are actually defining a ComponentClass and SystemClass respectively. From these instances of them can be created. They also act as identifiers for Concord.

For example. If you want to get a specific Component from an Entity, you'd do Component = Entity:get(ComponentClass). When ComponentClasses or SystemClasses are required it will be written clearly in the Documentation.

Containers

Since you'll be defining or creating lots of Components, Systems, Worlds and Assemblages, Concord adds container tables for each of them so that they are easily accessible.

These containers can be accessed through

local Components  = require("path.to.concord").components
local Systems     = require("path.to.concord").systems
local Worlds      = require("path.to.concord").worlds
local Assemblages = require("path.to.concord").aorlds

Concord has helper functions to fill these containers. There are the following options depending on your needs / preference:

-- Loads each file. They are put in the container according to it's filename ( 'src.components.component_1.lua' becomes 'component_1' )
Concord.loadComponents({"path.to.component_1", "path.to.component_2", "etc"})

-- Loads all files in the directory. They are put in the container according to it's filename ( 'src.components.component_1.lua' becomes 'component_1' )
Concord.loadComponents("path.to.directory.containing.components")


-- Put the ComponentClass into the container directly. Useful if you want more manual control. Note that you need to require the file in this case
Components.register("componentName", ComponentClass)

Things can then be accessed through their names:

local component_1_class   = Components.component_1
local componentName_class = Components.componentName

All the above applies the same to all the other containers.

Method chaining

-- All functions that do something ( eg. Don't return anything ) will return self
-- This allowes you to chain methods

entity
:give(position, 100, 50)
:give(velocity, 200, 0)
:remove(position)
:destroy()

--

world
:addEntity(fooEntity)
:addEntity(barEntity)
:clear()
:emit("test")

Components

When defining a ComponentClass you usually pass in a populate function. This will fill the Component with values.

-- Create the ComponentClass with a populate function
-- The component variable is the actual Component given to an Entity
-- The x and y variables are values we pass in when we create the Component 
local positionComponentClass = Concord.component(function(component, x, y)
    component.x = x or 0
    component.y = y or 0
end)

-- Create a ComponentClass without a populate function
-- Components of this type won't have any fields.
-- This can be useful to indiciate state.
local pushableComponentClass = Concord.component()
-- Manually register the ComponentClass to the container if we want
Concord.components.register("positionComponent", positionComponentClass)

-- Otherwise return the ComponentClass so it can be required
return positionComponentClass

Entities

Entities can be freely made and be given Components. You pass the ComponentClass and the values you want to pass. It will then create the Component for you.

Entities can only have a maximum of one of each Component. Entities can not share Components.

-- Create a new Entity
local myEntity = Entity() 
-- or
local myEntity = Entity(myWorld) -- To add it to a world immediately ( See World )
-- Give the entity the position Component defined above
-- x will become 100. y will become 50
myEntity:give(positionComponentClass, 100, 50)
-- Retrieve a Component
local positionComponent = myEntity[positionComponentClass]
-- or
local positionComponent = myEntity:get(positionComponentClass)

print(positionComponent.x, positionComponent.y) -- 100, 50
-- Remove a Component
myEntity:remove(positionComponentClass)
-- Check if the Entity has a Component
local hasPositionComponent = myEntity:has(positionComponentClass)
print(hasPositionComponent) -- false
-- Entity:give will override a Component if the Entity already has it
-- Entity:ensure will only put the Component if the Entity does not already have it

Entity:ensure(positionComponentClass, 0, 0) -- Will give
-- Position is {x = 0, y = 0}

Entity:give(positionComponentClass, 50, 50) -- Will override
-- Position is {x = 50, y = 50}

Entity:give(positionComponentClass, 100, 100) -- Will override
-- Position is {x = 100, y = 100}

Entity:ensure(positionComponentClass, 0, 0) -- Wont do anything
-- Position is {x = 100, y = 100}
-- Retrieve all Components
-- WARNING: Do not modify this table. It is read-only
local allComponents = myEntity:getComponents()

for ComponentClass, Component in ipairs(allComponents) do
    -- Do stuff
end
-- Assemble the Entity ( See Assemblages )
myEntity:assemble(myAssemblage, 100, true, "foo")
-- Check if the Entity is in a world
local inWorld = myEntity:inWorld()

-- Get the World the Entity is in
local world = myEntity:getWorld()
-- Destroy the Entity
myEntity:destroy()

Systems

Systems are defined as a SystemClass. Concord will automatically create an instance of a System when it is needed.

Systems get access to Entities through pools. They are created using a filter. Systems can have multiple pools.

-- Create a System
local mySystemClass = Concord.system({positionComponentClass}) -- Pool will contain all Entities with a position Component

-- Create a System with multiple pools
local mySystemClass = Concord.system(
    { -- This Pool's name will default to 'pool'
        positionCompomponentClass,
        velocityComponentClass,
    },
    { -- This Pool's name will be 'secondPool'
        healthComponentClass,
        damageableComponentClass,
        "secondPool",
    }
)
-- Manually register the SystemClass to the container if we want
Concord.system.register("mySystem", mySystemClass)

-- Otherwise return the SystemClass so it can be required
return mySystemClass
-- If a System has a :init function it will be called on creation

-- world is the World the System was created for
function mySystemClass:init(world)
    -- Do stuff
end
-- Defining a function
function mySystemClass:update(dt)
    -- Iterate over all entities in the Pool
    for _, e in ipairs(self.pool) 
        -- Get the Entity's Components
        local positionComponent = e[positionComponentClass]
        local velocityComponent = e[velocityComponentClass]

        -- Do something with the Components
        positionComponent.x = positionComponent.x + velocityComponent.x * dt
        positionComponent.y = positionComponent.y + velocityComponent.y * dt
    end


    -- Iterate over all entities in the second Pool
    for _, e in ipairs(self.secondPool) 
        -- Do something
    end
end
-- Systems can be enabled and disabled
-- Systems are enabled by default

-- Enable a System
mySystem:enable()
-- or
mySystem:setEnable(true)

-- Disable a System
mySystem:disable()
-- or
mySystem:setEnable(false)

-- Toggle the enable state
mySystem:toggleEnabled()

-- Get enabled state
local isEnabled = mySystem:isEnabled()
print(isEnabled) -- true
-- Get the World the System is in
local world = System:getWorld()

Worlds

Worlds are the thing your System and Entities live in. With Worlds you can :emit a callback. All Systems with this callback will then be called.

Worlds can have 1 instance of every SystemClass. Worlds can have any number of Entities.

-- Create World
local myWorld = Concord.world()
-- Manually register the World to the container if we want
Concord.worlds.register("myWorld", myWorld)

-- Otherwise return the World so it can be required
return myWorld
-- Add an Entity to the World
myWorld:addEntity(myEntity)

-- Remove an Entity from the World
myWorld:removeEntity(myEntity)
-- Add a System to the World
myWorld:addSystem(mySystemClass)

-- Add multiple Systems to the World
myWorld:addSystems(moveSystemClass, renderSystemClass, controlSystemClass)
-- Check if the World has a System
local hasSystem = myWorld:hasSystem(mySystemClass)

-- Get a System from the World
local mySystem = myWorld:getSystem(mySystemClass)
-- Emit an event

-- This will call the 'update' function of all added Systems if they have one
-- They will be called in the order they were added
myWorld:emit("update", dt)

-- You can emit any event with any parameters
myWorld:emit("customCallback", 100, true, "Hello World")
-- Remove all Entities from the World
myWorld:clear()
-- Override-able callbacks

-- Called when an Entity is added to the World
-- e is the Entity added
function myWorld:onEntityAdded(e)
    -- Do something
end

-- Called when an Entity is removed from the World
-- e is the Entity removed
function myWorld:onEntityRemoved(e)
    -- Do something
end

Assemblages

Assemblages are helpers to 'make' Entities something. An important distinction is that they append Components.

-- Make an Assemblage
-- e is the Entity being assembled.
-- cuteness and legs are variables passed in
local animalAssemblage(function(e, cuteness, legs)
    e
    :give(cutenessComponentClass, cuteness)
    :give(limbs, legs, 0) -- Variable amount of legs. 0 arm.
end)

-- Make an Assemblage that used animalAssemblage
-- cuteness is a variables passed in
local catAssemblage(function(e, cuteness)
    e
    :assemble(animalAssemblage, cuteness * 2, 4) -- Cats are twice as cute, and have 4 legs.
    :give(soundComponent, "meow.mp3")
end)
-- Use an Assemblage
myEntity:assemble(catAssemblage, 100) -- 100 cuteness
-- or
catAssemblage:assemble(myEntity, 100) -- 100 cuteness

Quick Example

local Concord = require("concord")

-- Defining ComponentClasses
-- I use UpperCamelCase to indicate its a class
local Position = Concord.component(function(c, x, y)
    c.x = x or 0
    c.y = y or 0
end)

local Velocity = Concord.component(function(c, x, y)
    c.x = x or 0
    c.y = y or 0
end)

local Drawable = Concord.component()


-- Defining Systems
local MoveSystem = Concord.system({Position, Velocity})

function MoveSystem:update(dt)
    for _, e in ipairs(self.pool) do
        -- I use lowerCamelCase to indicate its an instance
        local position = e[Position]
        local velocity = e[Velocity]

        position.x = position.x + velocity.x * dt
        position.y = position.y + velocity.y * dt
    end
end


local DrawSystem = Concord.System({Position, Drawable})

function DrawSystem:draw()
    for _, e in ipairs(self.pool) do
        local position = e[Position]
        
        love.graphics.circle("fill", position.x, position.y, 5)
    end
end


-- Create the World
local world = World()

-- Add the Systems
world:addSystems(MoveSystem, DrawSystem)

-- This Entity will be rendered on the screen, and move to the right at 100 pixels a second
local entity_1 = Concord.entity(world)
:give(Position, 100, 100)
:give(Velocity, 100, 0)
:give(Drawable)

-- This Entity will be rendered on the screen, and stay at 50, 50
local entity_2 = Concord.entity(world)
:give(Position, 50, 50)
:give(Drawable)

-- This Entity does exist in the World, but since it doesn't match any System's filters it won't do anything
local entity_3 = Concord.entity(world)
:give(Position, 200, 200)


-- Emit the events
function love.update(dt)
    world:emit(dt)
end

function love.draw()
    world:draw()
end

Contributors

  • Positive07: Constant support and a good rubberduck
  • Brbl: Early testing and issue reporting
  • Josh: Squashed a few bugs and generated docs
  • Erasio: I took inspiration from HooECS. He also introduced me to ECS
  • Speak: Lots of testing for new features of Concord
  • Tesselode: Brainstorming and helpful support

Licence

MIT Licensed - Copyright Justin van der Leij (Tjakka5)

concord's People

Contributors

keyslam avatar josh-perry avatar tesselode avatar tachytaenius avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.