updating docs for C#

pull/1/head
Evan Hemsley 2019-12-01 13:43:31 -08:00
parent d462a44421
commit 9a69b8f4f0
21 changed files with 246 additions and 140 deletions

1
.gitignore vendored
View File

@ -0,0 +1 @@
public

View File

@ -20,7 +20,7 @@ deploy:
# The output of our static site
local_dir: public
# The repository we are deploying to
repo: encompass-ecs/encompass-ecs.github.io
repo: MoonsideGames/moonsidegames.github.io
# The branch we are pushing the static repository
target_branch: master
# Information to use in the commit

View File

@ -1,4 +1,5 @@
baseURL = "/"
publishDir = "public/docs/encompass"
languageCode = "en-US"
title = "Encompass Docs"
theme = ["hugo-notice", "hugo-theme-learn"]

View File

@ -6,13 +6,8 @@ title: "Encompass"
[Encompass](https://github.com/encompass-ecs) is a powerful engine-agnostic framework to help you code games, or other kinds of simulations.
Object-oriented code is messy and rapidly becomes unmaintainable.
Object-oriented code scales and iterates poorly and rapidly becomes unmaintainable.
Encompass lets you write clean, de-coupled code so you can spend more time on your game design and less time fixing bugs.
Encompass is currently available with a TypeScript implementation that fully supports transpilation to Javascript and
[Lua](https://github.com/TypeScriptToLua/TypeScriptToLua).
A C# implementation is forthcoming.
Encompass lets you write clean, de-coupled code so you can spend more time on your game design and less time worrying about inheritance chains and fixing bugs.
If you are here to learn how to use Encompass and don't care about the justifications for it, or you've never made a game before, I recommend skipping ahead to [Chapter 2](getting_started).

View File

@ -6,28 +6,33 @@ weight: 5
A Component is a collection of related data.
To define a Component, extend the Component class.
To define a Component, declare a struct which implements the **IComponent** interface.
```ts
import { Component } from "encompass-ecs";
```cs
using Encompass;
using System.Numerics;
class PositionComponent extends Component {
public x: number;
public y: number;
public struct VelocityComponent : IComponent {
public Vector2 velocity;
}
```
Components are created in context with an Entity.
Components are attached to Entities with the **SetComponent** method.
```ts
const entity = World.create_entity();
const position = entity.add_component(PositionComponent);
position.x = 3;
position.y = -4;
```cs
using Encompass;
...
var worldBuilder = new WorldBuilder();
var entity = worldBuilder.CreateEntity();
worldBuilder.SetComponent(entity, new VelocityComponent { velocity = Vector2.Zero });
```
**SetComponent** can also be used from within an **Engine**. We will talk more about this later.
Components cannot exist apart from an Entity and are automagically destroyed when they are removed or their Entity is destroyed.
{{% notice warning %}}
Components should **never** reference other Components. This breaks the principle of loose coupling. You **will** regret it if you do this.
Components should **never** reference other Components directly. This breaks the principle of loose coupling. You **will** regret it if you do this.
{{% /notice %}}

View File

@ -10,29 +10,51 @@ An Engine is the Encompass notion of an ECS System. Much like the engine on a tr
I never liked the term System. It is typically used to mean structures in game design and I found this confusing when discussing code implementation vs design.
{{% /notice %}}
Engines are responsible for reading the game state, reading messages, emitting messages, and creating or mutating Entities and Components.
Engines may read any Entities and Components in the game world, read and send messages, and create and update Entities and Components.
An Engine which Reads a particular message is guaranteed to run *after* all Engines which Emit that particular message.
An Engine which Reads a particular message is guaranteed to run *after* all Engines which Send that particular message.
To define an Engine, extend the Engine class.
Here is an example Engine:
Let's say we wanted to allow Entities to temporarily pause their motion for a specified amount of time. Here is an example Engine:
```ts
import { Engine, Mutates, Reads } from "encompass-ecs";
import { LogoUIComponent } from "../../components/ui/logo";
import { ShowUIMessage } from "../../messages/show_ui";
```cs
using Encompass;
@Reads(ShowUIMessage)
@Mutates(LogoUIComponent)
export class LogoDisplayEngine extends Engine {
public update() {
const logo_ui_component = this.read_component_mutable(LogoUIComponent);
if (logo_ui_component && this.some(ShowUIMessage)) {
logo_ui_component.image.isVisible = true;
[Reads(typeof(PauseComponent))]
[Receives(typeof(PauseMessage))]
[Writes(typeof(PauseComponent))]
public class PauseEngine : Engine
{
public override void Update(double dt)
{
foreach (var (pauseComponent, entity) in ReadComponentsIncludingEntity<PauseComponent>())
{
var timer = pauseComponent.timer;
timer -= dt;
if (timer <= 0)
{
RemoveComponent<PauseComponent>(entity);
}
else
{
SetComponent(entity, new PauseComponent { timer = timer });
}
}
foreach (var pauseMessage in ReadMessages<PauseMessage>())
{
SetComponent(pauseMessage.entity, new PauseComponent
{
timer = pauseMessage.time
});
}
}
}
}
```
If a LogoUIComponent exists, and a ShowUIMessage is received, it will make the logo image on the LogoUIComponent visible. Simple!
This engine deals with a Component called a PauseComponent. PauseComponent has a timer which counts down based on delta-time. When the timer ticks past zero, the PauseComponent is removed. If a PauseMessage is received, a new PauseComponent is attached to the Entity specified by the message.
Notice that this Engine doesn't actually "do" the pausing, or even care if the Entity in question is capable of movement or not. In Engines that deals with movement, we can check if the Entities being moved have PauseComponents and modify how they are updated accordingly. This is the power of de-coupled logic at work.

View File

@ -8,12 +8,8 @@ An Entity is a structure composed of a unique ID and a collection of Components.
Entities do not have any implicit properties or behaviors. They are granted these by their collection of Components.
There is no limit to the amount of Components an Entity may have, and Entities can have any number of Components of a particular type.
There is no limit to the amount of Components an Entity may have, but Entities may only have a single Component of a particular type.
Entities are active by default and can be deactivated. They can also be destroyed, permanently removing them and their components from the World.
Entities can also be destroyed, permanently removing them and their components from the World.
Entities are created either by the WorldBuilder or by Engines. (We'll get into these soon.)
{{% notice warning %}}
You should **never** add methods or properties to an Entity. This is what Components are for.
{{% /notice %}}

View File

@ -8,19 +8,18 @@ Similar to Components, Messages are collections of data.
Messages are used to transmit data between Engines so they can manipulate the game state accordingly.
To define a message, extend the Message class.
To define a message, declare a struct which implements the IMessage interface.
```ts
import { Message } from "encompass-ecs";
```cs
using Encompass;
class MotionMessage extends Message {
public x: number;
public y: number;
public struct MotionMessage : IMessage {
public Vector2 motion;
}
```
Messages are temporary and destroyed at the end of the frame.
{{% notice notice %}}
Ok fine, since you asked, Messages actually live in an object pool so that they aren't garbage-collected at runtime. But you as the game developer don't have to worry about that.
Because structs are value types, we can create as many of them as we want without worrying about creating pressure on the garbage collector. Neato!
{{% /notice %}}

View File

@ -7,50 +7,91 @@ weight: 30
A Renderer is responsible for reading the game state and telling the game engine what to draw to the screen.
{{% notice notice %}}
Remember: Encompass isn't a game engine and it doesn't have a rendering system. So Renderers aren't actually doing the rendering, they're just telling the game engine what to render.
Remember: Encompass isn't a game engine and it doesn't have a rendering system. So Renderers aren't actually doing the rendering,it is just a way to structure how we tell the game engine what to render.
{{% /notice %}}
There are two kinds of renderers: GeneralRenderers and EntityRenderers.
There are two kinds of renderers: GeneralRenderers and OrderedRenderers.
A GeneralRenderer is a Renderer which reads the game state in order to draw elements to the screen. It also requires a layer, which represents the order in which it will draw to the screen.
If you were using the LOVE engine, a GeneralRenderer might look like this:
If you were using MonoGame, a GeneralRenderer might look like this:
```ts
import { GeneralRenderer } from "encompass-ecs";
import { ScoreComponent } from "game/components/score";
```cs
using System;
using Encompass;
using Microsoft.Xna.Framework;
using SamuraiGunn2.Components;
using SamuraiGunn2.Editor.Components;
using SamuraiGunn2.Helpers;
export class ScoreRenderer extends GeneralRenderer {
public layer = 4;
namespace SamuraiGunn2.Editor.Renderers
{
public class GridRenderer : GeneralRenderer
{
private int gridSize;
private PrimitiveDrawer primitiveDrawer;
public render() {
const score_component = this.read_component(ScoreComponent);
public GridRenderer(PrimitiveDrawer primitiveDrawer)
{
this.primitiveDrawer = primitiveDrawer;
this.gridSize = 16;
}
love.graphics.print(score_component.score, 20, 20);
public override void Render()
{
if (SomeComponent<EditorModeComponent>() && SomeComponent<MouseComponent>())
{
var entity = ReadEntity<MouseComponent>();
var transformComponent = GetComponent<TransformComponent>(entity);
Rectangle rectangle = new Rectangle
{
X = (transformComponent.Position.X / gridSize) * gridSize,
Y = (transformComponent.Position.Y / gridSize) * gridSize,
Width = gridSize,
Height = gridSize
};
primitiveDrawer.DrawBorder(rectangle, 0, new System.Numerics.Vector2(1, 1), Color.White, 1);
}
}
}
}
```
An EntityRenderer provides a structure for the common pattern of drawing an Entity which has a particular collection of Components and a specific type of DrawComponent. They also have the ability to draw DrawComponents at their specific layer.
GeneralRenderers are great for things like UI layers, where we always want a group of particular elements to be drawn at a specific layer regardless of the specifics of the game state.
If you were using the LOVE engine, a GeneralRenderer might look like this:
An OrderedRenderer provides a structure for the common pattern of wanting to draw an individual Component at a specific layer. OrderedRenderers must specify a component that implements IDrawableComponent.
```ts
import { EntityRenderer } from "encompass-ecs";
import { PointComponent } from "game/components/point";
import { PositionComponent } from "game/components/position";
If you were using MonoGame, an OrderedRenderer might look like this:
@Renders(PointComponent, PositionComponent)
export class PointRenderer extends EntityRenderer {
public render(entity: Entity) {
const point_component = entity.get_component(PointComponent);
const position_component = entity.get_component(PositionComponent);
```cs
using Encompass;
using Microsoft.Xna.Framework.Graphics;
using SamuraiGunn2.Components;
using SamuraiGunn2.Extensions;
using System;
using System.Numerics;
const color = point_component.color;
love.graphics.setColor(color.r, color.g, color.b, color.a);
love.graphics.point(position_component.x, position_component.y);
namespace SamuraiGunn2.Renderers
{
public class Texture2DRenderer : OrderedRenderer<Texture2DComponent>
{
private SpriteBatch spriteBatch;
public Texture2DRenderer(SpriteBatch spriteBatch)
{
this.spriteBatch = spriteBatch;
}
public override void Render(Entity entity, Texture2DComponent textureComponent)
{
var transformComponent = GetComponent<TransformComponent>(entity);
spriteBatch.Draw(textureComponent.texture, transformComponent.Position, null, textureComponent.color, transformComponent.Rotation, textureComponent.origin, transformComponent.Scale, SpriteEffects.None, 0);
}
}
}
```
For 2D games, you will need to use layers to be specific about the order in which entities draw. For a 3D game you will probably end up delegating rendering to some kind of scene/camera system.
For 2D games, you will need to use layers to be specific about the order in which elements are drawn to the screen. For a 3D game you will probably end up delegating most of the rendering to some kind of scene/camera system.

View File

@ -6,32 +6,59 @@ weight: 100
World is the pie crust that contains all the delicious Encompass ingredients together.
The World's *update* function drives the simulation and should be controlled from your engine's update loop.
The World's *Update* function drives the simulation and should be controlled from your engine's update loop.
The World's *draw* function tells the Renderers to draw the scene.
The World's *Draw* function tells the Renderers to draw the scene.
In LÖVE, the starter project game loop looks like this:
In MonoGame, the game loop looks something like this:
```ts
export class Game {
private world: World;
private canvas: Canvas;
```cs
using Encompass;
using Microsoft.Xna.Framework;
public class MyGame : Game
{
private World world;
SpriteBatch spriteBatch;
RenderTarget2D gameRenderTarget;
RenderTarget2D levelBrowserRenderTarget;
RenderTarget2D uiRenderTarget;
...
public update(dt: number) {
this.world.update(dt);
/// <summary>
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input, and playing audio.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(Microsoft.Xna.Framework.PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
Exit();
world.Update(gameTime.ElapsedGameTime.TotalSeconds);
base.Update(gameTime);
}
public draw() {
love.graphics.clear();
love.graphics.setCanvas(this.canvas);
love.graphics.clear();
this.world.draw();
love.graphics.setCanvas();
love.graphics.setBlendMode("alpha", "premultiplied");
love.graphics.setColor(1, 1, 1, 1);
love.graphics.draw(this.canvas);
/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw(GameTime gameTime)
{
world.Draw();
GraphicsDevice.SetRenderTarget(null);
spriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp);
spriteBatch.Draw(gameRenderTarget, windowDimensions, Color.White);
spriteBatch.Draw(levelBrowserRenderTarget, windowDimensions, Color.White);
spriteBatch.Draw(uiRenderTarget, windowDimensions, Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
```
@ -46,4 +73,6 @@ Certain Encompass projects actually have multiple separate Worlds to manage cert
*dt* stands for delta-time. Correct usage of delta-time is crucial to make sure that your game does not become *frame-dependent*, which is very bad. We'll talk more about frame-dependence later in the tutorial, but to briefly summarize, if your game is frame-dependent you will run into very frustrating behavior when running your game on different computer systems.
Even if you lock your game to a fixed timestep, writing your game with delta-time in mind can be the difference between changing the timestep being a one-line tweak or a weeks long hair-pulling nightmare.
That's it! Now that we have these high-level concepts down, let's build an actual, for-real game.

View File

@ -6,45 +6,49 @@ weight: 35
WorldBuilder is used to construct a World from Engines, Renderers, and an initial state of Entities, Components, and Messages.
The WorldBuilder enforces certain rules about Engine structure. It is forbidden to have messages create cycles between Engines, and no Component may be mutated by more than one Engine.
The WorldBuilder enforces certain rules about Engine structure. It is forbidden to have messages create cycles between Engines, and no Component may be mutated by more than one Engine without declaring a priority.
The WorldBuilder uses Engines and their Message read/emit information to determine a valid ordering of the Engines, which is given to the World.
The WorldBuilder uses Engines and their Message read/send information to determine a valid ordering of the Engines, which is given to the World.
Here is an example usage:
Here is an example usage with MonoGame:
```ts
import { WorldBuilder } from "encompass-ecs";
import { CanvasComponent } from "./components/canvas";
import { PositionComponent } from "./components/position";
import { VelocityComponent } from "./components/velocity";
import { MotionEngine } from "./engines/motion";
import { CanvasRenderer } from "./renderers/canvas";
```cs
using Encompass;
using Microsoft.Xna.Framework;
class Game {
public class MyGame : Game
{
private World world;
...
public load() {
const world_builder = new WorldBuilder();
protected override void LoadContent()
{
var worldBuilder = new WorldBuilder();
world_builder.add_engine(MotionEngine);
world_builder.add_renderer(CanvasRenderer);
worldBuilder.AddEngine(new MotionEngine());
worldBuilder.AddEngine(new TextureRenderer());
const entity = world_builder.create_entity();
var entity = worldBuilder.CreateEntity();
const position_component = entity.add_component(PositionComponent);
position_component.x = 0;
position_component.y = 0;
SetComponent(entity, new PositionComponent
{
x = 0,
y = 0
});
const velocity_component = entity.add_component(VelocityComponent);
velocity_component.x = 20;
velocity_component.y = 0;
SetComponent(entity, new VelocityComponent
{
x = 20,
y = 0
});
const sprite_component = entity.add_component(SpriteComponent);
canvas_component.canvas = love.graphics.newImage("assets/sprites/ball.png");
SetComponent(entity, new TextureComponent
{
texture = TextureHelper.LoadTexture("assets/sprites/ball.png")
});
this.world = world_builder.build();
world = worldBuilder.Build();
}
...
@ -54,5 +58,5 @@ class Game {
Now our game will initialize with a ball that moves horizontally across the screen!
{{% notice tip %}}
Make sure that you remember to add Engines to the WorldBuilder when you define them. Otherwise nothing will happen, which can be very embarrassing.
Be extra careful that you remember to add Engines to the WorldBuilder when you define them. Otherwise nothing will happen, which can be very embarrassing.
{{% /notice %}}

View File

@ -16,17 +16,16 @@ That means it needs to run on top of an engine.
Ultimately, this is a question that you have to answer for your project. Is there an engine you're already comfortable with? Which platforms are you targeting? Linux? PS4? Android? Are there any features you would really like to have, like a built-in physics simulator? These are questions that could help you choose an engine.
Encompass-TS can hook into any engine that supports JavaScript or Lua scripting.
Encompass-CS can hook into any engine that supports C# scripting. (**NOTE:** Encompass-CS is not done yet.)
Encompass-CS can hook into any engine that supports C# scripting.
So you have a lot of choices!
Here are some engines that I have used:
[LÖVE](https://love2d.org/) is a wonderful little framework for 2D games. It's cross-platform and very lightweight, but with a lot of powerful features, including a complete physics simulator. It uses Lua scripting, so you would want Encompass-TS.
[MonoGame](http://www.monogame.net/) is a cross-platform 2D/3D framework that thousands of games have used. You can use it to ship games on basically any platform that exists and it is extremely well-supported. It uses C# scripting.
[BabylonJS](https://www.babylonjs.com/) uses the power of WebGL to run 3D games in the browser. It has a powerful graphics pipeline and you can make stuff that has some wow factor. It runs on JS.
FNA
Unity uses C# scripting, but you would have to adapt it in certain ways to the bastardized Unity architecture. I personally have never tried this but you are certainly welcome to give it a shot!
Encompass gives you the power to develop using many different engines, so feel free to experiment and find one you like! And if you switch to an engine that uses the same scripting language, it's actually very easy to switch engines, because the simulation layer is mostly self-contained.

View File

@ -6,6 +6,6 @@ weight: 9
You will want some kind of text editor to develop Encompass projects.
I _highly_ recommend [VSCodium](https://vscodium.com/) if you are on Windows or OSX, and Code - OSS if you are on Linux. These are open-source distributions of Microsoft's VSCode editor, which features excellent Typescript integration and various convenient features, like an integrated Git interface and terminal. (Make sure you set the terminal to Git Bash if you are on Windows - this is under File -> Settings.)
I _highly_ recommend [VSCodium](https://vscodium.com/) if you are on Windows or OSX, and Code - OSS if you are on Linux. These are open-source distributions of Microsoft's VSCode editor, which features excellent C# integration and various convenient features, like an integrated Git interface and terminal. (Make sure you set the terminal to Git Bash if you are on Windows - this is under File -> Settings.)
Of course, if you prefer some other editor, that will be perfectly fine.

View File

@ -10,28 +10,28 @@ The core of the architecture is the introduction of a new construct to ECS: the
A Message is fundamentally a variant of Component, in that it only contains data. But, it is designed to be temporary and is discarded at the end of each frame. It is used to communicate useful information between Systems.
We also introduce some extra information to Systems. Each System must declare the Messages that it **Reads**, the Messages that it **Emits**, and the Components that it **Mutates**.
We also introduce some extra information to Systems. Each System must declare the Messages that it **Receives**, the Messages that it **Sends**, the Components that it **Reads** and the Components that it **Writes**.
Let's go back to our earlier example.
We have TransformComponent, which contains position and orientation data, and VelocityComponent, which contains an *x* and *y* component for linear motion.
Our MotionDetecterSystem reads each Entity that has both a TransformComponent and a VelocityComponent, and emits a MotionMessage, which contains a reference to the specific TransformComponent and the *x* and *y* velocity given by the VelocityComponent.
Our MotionDetecterSystem reads each Entity that has both a TransformComponent and a VelocityComponent, and emits a MotionMessage, which contains a reference to the Entity and the *x* and *y* velocity given by the VelocityComponent.
We also have a TeleportSystem that needs to teleport the character forward a bit. Let's say when the player presses the X button, a TeleportMessage is fired. The TeleportSystem reads this message and emits a MotionMessage in response.
We also have a **TeleportSystem** that needs to teleport the character forward a bit. Let's say when the player presses the X button, a TeleportMessage is fired. The TeleportSystem reads this message and emits a MotionMessage in response.
Now we have our MotionSystem. The MotionSystem declares that it Mutates the TransformComponent, reads the MotionMessages that apply to each TransformComponent, and applies them simultaneously, adding their *x* and *y* values to the TransformComponent. Voilà! No race conditions! And we can re-use similar behaviors easily without re-writing code by consolidating Messages.
Now we have our **MotionSystem**. The MotionSystem declares that it Mutates the TransformComponent, reads the MotionMessages that apply to each TransformComponent, and applies them simultaneously, adding their *x* and *y* values to the TransformComponent. Voilà! No race conditions! We can re-use similar behaviors easily without re-writing code by consolidating Messages.
You might be wondering: how does the game know which order these systems need to be in? Well...
**Hyper ECS figures it out for you.**
That's right! With the power of graph theory, we can construct an order for our Systems so that any System which Emits a certain Message runs before any System that Reads the same Message. This means, when you write behavior for your game, you *never* have to specify the order in which your Systems run. You simply write code, and the Systems run in a valid order, every time, without surprising you.
That's right! With the power of graph theory, we can construct an order for our Systems so that any System which Sends a certain Message runs before any System that Reads the same Message. This means, when you write behavior for your game, you *never* have to specify the order in which your Systems run. You simply write code, and the Systems run in a valid order, every time, without surprising you.
Of course, to accomplish this, there are some restrictions that your Systems must follow.
Systems are not allowed to create message cycles. If System A emits Message B, which is read by System B which emits Message C, which is read by System A, then we cannot create a valid ordering of Systems. This is not a flaw in the architecture: A message cycle is simply evidence that you haven't quite thought through what your Systems are doing, and can generally be easily eliminated by the introduction of a new System.
Systems are not allowed to create message cycles. If System A sends Message B, which is read by System B which emits Message C, which is read by System A, then we cannot create a valid ordering of Systems. This is not a flaw in the architecture: A message cycle is simply evidence that you haven't quite thought through what your Systems are doing, and can generally be easily eliminated by the introduction of a new System.
Two separate systems are not allowed to Mutate the same Component. Obviously, if we allowed this, we would introduce the possibility of two Systems changing the same component, creating a race condition. If we have two Systems where it makes sense to change the same Component, we can create a new Message and System to consolidate the changes, and avoid race conditions.
The other restriction involves two separate systems which Write the same Component. They may do so, but they must declare a priority. Obviously, if we allowed Systems to Write components willy-nilly, we would introduce the possibility of two Systems changing the same component on the same frame, creating a race condition. If we have two Systems where it makes sense to change the same Component, we can either create a new Message and System to consolidate the changes, or we can declare write priority so that one System's changes always override the other's. This way we can avoid race conditions.
If you are used to programming games in an object-oriented way, you will likely find the ECS pattern counter-intuitive at first. But once you learn to think in a Hyper ECS way, you will be shocked at how flexible and simple your programs become.
If you are used to programming games in an object-oriented way, you will likely find the pattern counter-intuitive at first. But once you learn to think in a Hyper ECS way, you will be shocked at how flexible and simple your programs become.

File diff suppressed because one or more lines are too long

View File

@ -250,5 +250,5 @@ figcaption h4 {
}
.is-sticky #top-bar {
box-shadow: -1px 2px 5px 1px rgba(0, 0, 0, 0.1);
}
box-shadow: -1px 2px 5px 1px rgba(0, 0, 0, 0.1);
}

View File

@ -26,6 +26,20 @@
font-style: normal;
font-weight: 200;
}
@font-face {
font-family: 'Public Sans';
src: url("../fonts/publicsans-regular-webfont.woff") format("woff");
src: url("../fonts/publicsans-regular-webfont.woff2") format("woff2");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: 'Public Sans';
src: url("../fonts/publicsans-light-webfont.woff") format("woff");
src: url("../fonts/publicsans-light-webfont.woff2") format("woff2");
font-style: normal;
font-weight: 300;
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
@ -448,10 +462,10 @@ textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[typ
margin: 0;
}
body {
font-family: "Work Sans", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif;
font-family: "Public Sans", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif;
font-weight: 300;
line-height: 1.6;
font-size: 18px !important;
font-size: 16px !important;
}
h2, h3, h4, h5, h6 {
font-family: "Work Sans", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif;