Skip to content

ERA C Overview

Auz edited this page May 27, 2024 · 25 revisions

Table of Contents

Introduction

Note

This page assumes you are already familiar with basic programming concepts and data types.

ERA-3D games are programmed in a scripting language called ERA-C. ERA-C is similar to C in many ways, but has a few key differences that are worth noting.

If you are already familiar with C, see the Differences From C section.

ERA-C Syntax

Comments

Comments in ERA-C follow the same syntax as C/C++.

// single-line comment
int a = 32; // another comment
/*
multi
line
comment
*/

Multi-line comments do support nesting. (Though GitHub's syntax highlighting does not.)

/*
  this is a comment
  /*
    this is a nested comment
  */
  this is still a comment
*/

Variables

Both local and global variables are declared using the pattern <data type> <name> ;.

Variable names must start with a letter and can contain the characters a-z, A-Z, 0-9, _

int my_int; // an int variable
float my_float; // a float variable

Local variables (variables declared inside a function or loop scope) can be assigned a value when they are declared.

int foo = 42;

Global variables must be initialized inside of a function.

The init() function is a good place for that. (See the Hook Functions section)

Primitive Types

Primitive types are data types built into the ERA-C compiler.

int

int is a data type that is capable of storing positive and negative whole numbers.

Specifically, they are 32-bit integers. Most operations treat them as signed integers, but some operators treat int values as unsigned.

Integer literal values can be given in decimal form, but they can also be represented in hexadecimal and binary form using the 0x and 0b prefixes.

Unlike C, leading zeros do not mean a number is in octal format.

int a = 24; // positive value
int b = -24; // negative value
int hex = 0xFF; // hexadecimal value (255)
int binary = 0b10101010; // binary value (170)

// To make large numbers easier to read, underscores can be inserted between digits.
int big_number = 12_345_678;

// Character literals are also supported, and are interpreted as int values.
int letter_e = 'E';

int Operations

ints support various arithmetic operations, as well as bitwise and logical ones.

+, -, *, /, and % are the addition, subtraction, multiplication, division, and modulus operators. They follow the same precedence rules as C.

int a = 2 + 2;
int b = 5 - 3;
int c = 9 * 100;
int d = 10 / 2;
int e = 10 % 5;

Note

Dividing int values does not result in a fractional value, the fractional component is discarded instead.

5 / 2 returns 2, not 2.5.

ERA-C also supports bitwise operations for integers.

int a = ~0b00001111; // bitwise NOT: a = 0b11110000
int b = 0b10101010 | 0b01010101; // bitwise OR: b = 0b11111111
int c = 0b10101111 & 0b00111100; // bitwise AND: c = 0b00101100
int d = 0b11001100 ^ 0b10101010; // bitwise XOR: d = 0b01100110
int e = 0b00001111 << 2; // bitwise left shift: e = 0b00111100
int f = 0b11110000 >> 4; // bitwise right shift (logical): f = 0b00001111
int g = 0b10000000 >>> 4; // bitwise right shift (arithmetic): f = 0b11111000

The logical and comparison operators return an integer value, so their result can be stored in an int.

If the comparison is true, a value of 1 is returned. Otherwise, 0 is returned.

int a = 1 < 2; // a = 1 (true)
int b = 1 == 2; // b = 0 (false)

float

floats are 32-bit floating point values, meaning they are numbers with a fractional component.

float my_float = 2.5;

float Operations

floats support the +, -, *, /, and % operators, as well as the comparison operators.

vec2

vec2 represents a 2D vector with x and y components.

x and y both have the float data type.

vec2 v = vec2(1.0, 2.0); // 2D vector with the value (x=1.0, y=2.0)

v.x = 9.0; // assigning a new x value
v.y = 0.25; // assigning a new y value

The following table lists special functions used to construct vec2s with common values:

Function vec2 Return Value
vec2Zero() {0.0, 0.0} (equivalent to vec2() with no arguments)
vec2One() {1.0, 1.0}
vec2Up() {0.0, -1.0}
vec2Down() {0.0, 1.0}
vec2Left() {-1.0, 0.0}
vec2Right() {1.0, 0.0}

vec3

vec3 represents a 3D vector with x, y, and z components.

x, y, and z all have the float data type.

vec3 v = vec3(1.0, 2.0, 3.0); // 3D vector with the value (x=1.0, y=2.0, z=3.0)

v.x = 7.5; // assigning a new x value
v.y = 0.75; // assigning a new y value
v.z = 4.9; // assigning a new z value

The following table lists special functions used to construct vec3s with common values:

Function vec3 Return Value
vec3Zero() {0.0, 0.0, 0.0} (equivalent to vec3() with no arguments)
vec3One() {1.0, 1.0, 1.0}
vec3Up() {0.0, 1.0, 0.0}
vec3Down() {0.0, -1.0, 0.0}
vec3Left() {-1.0, 0.0, 0.0}
vec3Right() {1.0, 0.0, 0.0}
vec3Forward() {0.0, 0.0, -1.0}
vec3Back() {0.0, 0.0, 1.0}

Vector Operations

Both vec2 and vec3 support addition, subtraction, multiplication, and division.

Vector types cannot be mixed in vector arithmetic, only vectors of the same dimensions can be added, subtracted, etc.

During a arithmetic operation, the individual members of the vector are operated on. For example, when adding two vec2 values, the result will be vec2(a.x + b.x, a.y + b.y). The same pattern applies to all arithmetic operators.

vec2 v2 = vec2(1.1, 2.2) + vec2(3.3, 4.4); // v2 = vec2(4.4, 6.6)
vec3 v3 = vec3(1.1, 2.2, 3.3) + vec3(4.4, 5.5, 6.6); // v3 = vec3(5.5, 7.7, 9.9)

Vectors can also be multiplied and divided by int and float values. This will return a vector with each member having been multiplied/divided by the int/float value.

vec3 a = vec3(1.1, 2.2, 3.3) * 2; // a = vec3(2.2, 4.4, 6.6)
vec2 b = vec2(5.0, 10.0) / 2.5; // b = vec2(2.0, 4.0)

Vectors support all comparison operators, and can only be compared with their same type.

Comparing vectors compares each member pair, and the comparison is only true if all pairs satisfy the comparison.

vec2 a = vec2(10.0, 20.0);
vec2 b = vec2(1.0, 2.0);

int result = a < b; result = 1 (true), as a.x < b.x and a.y < b.y

string

The string data type is used to hold text.

string literals must be enclosed within double-quotes (").

string text = "hello world!";

To fetch a character from the string, or to change a character, use the following expression: <variable> [ <index> ];.

The characters of a string are zero-indexed, meaning that the first character is stored at index 0.

string text = "hello world!";
int character = text[0]; // fetch a character
text[1] = 'E'; // set the second character in the string to 'E'

Internally, string is a struct (see the Structs section) that looks like this:

struct string {
  int length;
  void* data;
};

string Operations

Strings support the == and != operators. Two strings are considered equal if the following conditions are met:

  1. both strings are the same length
  2. both strings point to the same address or the characters in both strings match

Arrays

Arrays are essentially blocks of memory used to store a list of elements all of a particular data type. For example, you might have an array containing 10 int values, or an array containing 5 vec2 values.

Arrays are fixed in size, once they are declared the cannot be resized.

int my_array[10]; // declare an array of 10 ints

Access array elements using the pattern <array variable> [ <index> ], where <index> is an integer value.

Array values are zero-indexed; meaning the first element in the array is stored at index 0, the second at index 1, and so on.

int my_array[10];
int x = my_array[0]; // get the first value stored in the array
my_array[1] = 99; // store a new value in the array at index 1

Pointers

ERA-C supports pointers, which behave similarly to pointers in C. To declare a pointer to a type, put a * after the data type.

int* int_pointer; // pointer to an int

To obtain the memory address of a variable, use the address-of operator. (&)

int x = 42; // normal int variable
int* ptr = &x; // ptr now contains the memory address of x 

The unary dereference operator (*) can be used to fetch the value pointed to by a pointer.

int x = 42;
int* ptr = &x;
int y = *ptr; // y now contains the same value as x

Like arrays, pointers can be used to access multiple contiguous values stored next to each other in memory.

int* p = 0; // in this example, p points to memory address 0, which corresponds to general-purpose memory.
int first = p[0]; // get the first value stored at the address p points to
p[1] = 100; // store a new value at the address p points to plus an offset of 1 int. (which would be the address 4. 1 int = 4 bytes)

A pointer with the type void* is a special type of pointer that can point to any data type. Pointers of this type are called "void pointers".

Void pointers cannot be dereferenced, they are only used as a container for arbitrary memory addresses.

Any pointer type and be assigned the value of a void pointer, and vice versa.

int i = 100;
float f = 32.2;
void* p = &i; // p points to i
int x = *p; // ERROR: cannot dereference void pointers!
p = &f; // p now points to f

Pointers can be assigned arbitrary integer values, and thus arbitrary memory addresses.

In C, this is dangerous and discouraged most of the time, but in ERA-C it is needed to access specific parts of memory.

int* p = 0x00800000; // p now points to address 0x00800000, which is the beginning of the system's texture memory

Warning

Pointers in ERA-C must be aligned when dereferencing them, meaning the address they contain must be a multiple of the size of the type they point to (or address 0). If a pointer is misaligned when it is dereferenced, en error will be raised and the program will halt.

Pointers can be added to or subtracted from, changing the address they point to. This is referred to as "pointer arithmetic".

Adding a value of 1 to a pointer will increase its address by the size of the data type it points to.

For example, adding 1 to a int* will increase its address by 4, since ints are 4 bytes in size.

Note

Void pointers do not support pointer arithmetic.

int* p = 0;
p = p + 1; // p now points to the next int in memory

The sizeof() operator will return the size of a given type in bytes.

int size = sizeof(int); // size = 4

null is a special constant that represents an invalid memory address. Unlike C's NULL, it is not equal to the integer value 0.

Assign a pointer to null to signify that it does not currently point to a valid address.

void* p = null;

Structs

Structs are compound data types that are made up of other smaller types. They are useful for representing complex objects with many attributes.

struct MyStruct {
  int a;
  float b;
  int c;
};

In the above example, the struct MyStruct is declared, and contains an int and float members.

Note

Structs must be declared outside of any function, and before they are used as a variable type.

To create a struct instance variable, use the struct name as the variable type.

MyStruct instance;

Struct instances can be created using the pattern <struct name> ( <member values...> )

Mystruct instance = Mystruct(1, 2.5, 3); // create a MyStruct instance with the value (a=1, b=2.5, c=3)

C-style struct initializers are also allowed.

These use the pattern ( <struct name> ) { <member values...> }

Mystruct instance = (Mystruct){1, 2.5, 3}; // create a MyStruct instance with the value (a=1, b=2.5, c=3)

If a struct initializer's member value list is left empty, all struct members will be initialized to zero.

MyStruct instance = MyStruct(); // instance = (0, 0.0, 0)
vec3 v = vec3(); // v = (0.0, 0.0, 0.0)

To access struct members, use the dot operator. (.)

Mystruct instance = MyStruct(1, 2.5, 3);
int x = instance.a; // x now contains 1
instance.b = 10.1; // set the b member of instance to a new value

When accessing a struct using a pointer, the arrow operator (->) must be used instead of the dot operator.

Mystruct instance = MyStruct(1, 2.5, 3);
MyInstance* p = &instance; // p points to instance
p -> a = 22; // access the a member of instance using the pointer p

Function Pointers

Function pointers are special pointers that point to code, rather than data.

To declare a function pointer, use the pattern <return type> ( <argument types...> ) <variable name> ;

void(int, float) func_ptr; // func_ptr points to a function that accepts an `int` and `float` argument and does not return a value.

Assign a function pointer by using the name of the function to point to.

void my_function(int a, float b) {
  // ... function code here ...
}

void(int, float) func_ptr = my_function;

Function pointers do not support pointer arithmetic, and can only be cast to void*.

Function pointers can be assigned null.

Type Casting

Type casting in ERA-C works much like C. To cast an expression to another type, use the pattern ( <type> ) <expression>.

float f = 2.5;
int i = (int)f; // cast the float value of f to an int

float to int casting discards the fractional component of the float. (The value is truncated)

In the above example, i would have the value 2 after the cast.

The following table lists all accepted casts:

Original Type Target Type Result
int float float equal to the original integer value.
int Non-function pointer Pointer with address equal to the original integer value.
float int int containing the truncated integer value of the float.
Non-function pointer int int containing the address of the original pointer value.
Function pointer void* void* containing the address of the original function pointer value.
void* Function pointer Function pointer containing the address of the original void pointer value.

Enums

Enums are lists of compile-time constants used to represent numerical values.

// declaring an enum
enum {
  VALUE_A = 0, // enum entries can be assigned direct values
  VALUE_B, // when no value is given, an entry is assigned a value that is 1 higher than the previous entry
  VALUE_C
};

// using the enum entries
int x = VALUE_C; // x = 2

Note

Enums must be declared outside of any function, and before their members are used in an expression.

Operators

ERA-C has the following operators:

Operator Description
+ Addition
- Subtraction
* Multiplication
/ Division
% Modulus
! Logical Not (Unary)
&& Logical And
|| Logical Or
== Equal-to Comparision
!= Not-Equal-to Comparision
< Less-Than Comparision
> Greater-Than Comparision
<= Less-Than-or-Equal-to Comparision
>= Greater-Than-or-Equal-to Comparision
~ Bitwise NOT (Unary)
& Bitwise AND
| Bitwise OR
^ Bitwise XOR
<< Bitwise Left Shift
>> Bitwise Logical Right Shift
>>> Bitwise Arithmetic Right Shift
. Struct Member Access
-> Struct Member Access (Pointer)
sizeof() Type Size (in bytes)

Control Flow

if statements work just like in C.

int x = 1;

if (x == 1) {
  // do something...
}
else if (x == 2) {
  // do something else...
}
else {
  // no previous conditions were true
}

int values can be used as conditions. A value of 0 is false, all non-zero values are true.

Pointers and function pointers can be used as the if statement condition to check if they point to valid addresses.

void* p = null;

if (p) {
  // p points to a valid address
}
else {
  // p is not valid
}

Note

The expression my_pointer == null will only be true if my_pointer contains exactly the value null.

Loops

ERA-C supports for and while loops with the same syntax as C.

for (int i = 0; i < 10; i = i + 1) {
  // do something...
}
while (a == b) {
  // do something...
}

The break keyword will end the current loop immediately.

for (int i = 0; i < 10; i = i + 1) {
  if (i == 5) {
    break; // exit the loop body
  }
  
  // do something with i...
}

The continue keyword will skip to the next loop iteration.

for (int i = 0; i < 10; i = i + 1) {
  if (i == 5) {
    continue; // the rest of the loop body is skipped, and the loop starts again after incrementing i
  }
  
  // do something with i...
}

Functions

Functions in ERA-C follow mostly the same syntax as C.

// declaring a function
void my_function(int a, float b) {
   // do something...
}

// calling the function
my_function(1, 2.0);

The return keyword returns a value from a function.

int add(int a, int b) {
   return a + b;
}

Note

Unlike C, a function that lists no arguments is a function that accepts no arguments.

The C style void my_function(void) will not compile in ERA-C.

Vararg Functions

Functions that accept a variable number of arguments are called "vararg" or "variadic" functions.

The special vararg type (...) is used to indicate that a function is variadic.

The vararg type must come after all other non-optional arguments.

// declaring a variadic function
void my_function(int a, float b, ...) {
  // do something...
}

// calling the function
my_function(1, 2.0, 3, 4, 5);

To access vararg arguments, use the vargc() and vargv() API functions.

vargc() takes no arguments and returns the remaining number of bytes available in the varargs list.

if (vargc() >= 4) {
  // there are at least 4 bytes of vararg data left.
}

vargv() returns a void* pointing to the current vararg argument, and accepts a size in bytes as an argument.

The size given to vargv() is the offset to give the vararg pointer in order to move past the current argument.

It is recommended to use sizeof() to get argument size.

int sum(...) {
  int result = 0;
  while (vargc() >= sizeof(int)) {
    int* arg_pointer = (int*) vargv(sizeof(int));
    result = result + *arg_pointer;
  }
  return result;
}

// calling sum()
int value = sum(1, 2, 3, 4, 5); // value = 15

The above snippet is an example of a simple sum function that adds a variable number of integers.

Hook Functions

ERA-3D recognizes special "hook" functions declared in a ERA-C program and will call them at certain times.

At least one hook function must be declared in order for a cart to run.

init

if a function is declared with the signature void init(), it will be called once when a cart is ran.

Use this function to initialize game state.

void init() {
  // initialize state here...
}

update

if a function is declared with the signature void update(float delta_time), it will be called once every frame.

Use this function to update game state. (check inputs, move objects, etc.)

delta_time will be passed to the function automatically, and will contain the amount of time (in seconds) since the last frame.

void update(float delta_time) {
  // update state here...
}

Note

The delta_time argument for update() can be given any other name, but it must be a float.

draw

if a function is declared with the signature void draw(), it will be called once every frame after the call to update().

Use this function to configure rendering settings and to render graphics to the screen.

void draw() {
  // render graphics here...
}

Built-in Constants

The ERA-C compiler has several built-in constants for use with various functions. The following table lists their names and values:

Name Value
true 1
false 0
null 0xFFFFFFFF
SCREEN_WIDTH 480
SCREEN_HEIGHT 360
FONT_WIDTH 6
FONT_HEIGHT 9
MESH_LINES 0
MESH_TRIANGLES 1
MESH_QUADS 2
MAT_PROJECTION 0
MAT_MODELVIEW 1
CLEAR_NONE 0
CLEAR_COLOR 0b100
CLEAR_DEPTH 0b010
CLEAR_STENCIL 0b001
CLEAR_ALL 0b111
COLOR_NONE 0
COLOR_R 0b1000
COLOR_G 0b0100
COLOR_B 0b0010
COLOR_A 0b0001
COLOR_ALL 0b1111
TEXTURE_WRAP 0
TEXTURE_CLAMP 1
TEXTURE_NONE 2
CULL_BACK 0
CULL_FRONT 1
CULL_NONE 2
BLEND_ALPHA 0
BLEND_ADD 1
BLEND_MULTIPLY 2
BLEND_ADD_ALT 3
BLEND_SUBTRACT 4
BLEND_PREMULTIPLY 5
BLEND_CUSTOM 6
BLEND_CUSTOM_EX 7
CAM_PERSPECTIVE 0
CAM_ORTHOGRAPHIC 1
LIGHT_POINT 0
LIGHT_DIRECTIONAL 1
FACE_FRONT 0
FACE_BACK 1
FACE_BOTH 2
FUNC_LESS 0
FUNC_LEQUAL 1
FUNC_GREATER 2
FUNC_GEQUAL 3
FUNC_EQUAL 4
FUNC_NOTEQUAL 5
FUNC_ALWAYS 6
FUNC_NEVER 7
FACTOR_ZERO 0
FACTOR_ONE 1
FACTOR_SRC_RGB 2
FACTOR_ONE_MINUS_SRC_RGB 3
FACTOR_DST_RGB 4
FACTOR_ONE_MINUS_DST_RGB 5
FACTOR_SRC_ALPHA 6
FACTOR_ONE_MINUS_SRC_ALPHA 7
FACTOR_DST_ALPHA 8
FACTOR_ONE_MINUS_DST_ALPHA 9
FACTOR_CONSTANT_RGB 10
FACTOR_ONE_MINUS_CONSTANT_RGB 11
FACTOR_CONSTANT_ALPHA 12
FACTOR_ONE_MINUS_CONSTANT_ALPHA 13
FACTOR_SRC_ALPHA_SATURATE 14
EQ_ADD 0
EQ_SUBTRACT 1
EQ_SUBTRACT_REVERSE 2
EQ_MIN 3
EQ_MAX 4
STENCIL_KEEP 0
STENCIL_REPLACE 1
STENCIL_INC 2
STENCIL_INC_WRAP 3
STENCIL_DEC 4
STENCIL_DEC_WRAP 5
STENCIL_ZERO 6
STENCIL_INVERT 7
BTN_UP 0
BTN_DOWN 1
BTN_LEFT 2
BTN_RIGHT 3
BTN_TRIANGLE 4
BTN_CROSS 5
BTN_SQUARE 6
BTN_CIRCLE 7
BTN_L1 8
BTN_L2 9
BTN_R1 10
BTN_R2 11
BTN_SELECT 12
BTN_START 13

Built-in Structs

ERA-C has multiple built-in struct types for representing various objects. Unlike the vector and string types, which behave like structs in certain situations, these built-in structs do not have any custom functionality built into the compiler.

matrix

The matrix struct is a 4x4 matrix used for storing transformations like translation, rotation, and scale.

TODO: explain member ordering

struct matrix {
  float m0; float m4; float m8; float m12;  // first row
  float m1; float m5; float m9; float m13;  // second row
  float m2; float m6; float m10; float m14; // third row
  float m3; float m7; float m11; float m15; // fourth row
};

vertex

The vertex struct contains 3D vertex data according to the order that vertex data is stored in OBJMEM and OBJBANKs.

Thus, OBJMEM and OBJBANKs can be treated as arrays of vertex structs.

struct vertex {
  vec3 position;
  vec3 normal;
  vec2 uv;
  int color;
};

cam2d

In SYSMEM, there are 4 memory-mapped 2D cameras that can be used for configuring the view.

To get a pointer to a 2D camera, use the getCam2D() function.

cam2d* cam = getCam2D(0); // get a pointer to 2D camera 0

To configure the view to a built-in 2D camera, use the camera2D() function.

camera2D(0); // use 2D camera 0

Note

When camera2D() is called, depth testing is automatically disabled.

The cam2d struct has the following definition:

struct cam2d {
  vec2 offset;
  vec2 target;
  float rotation;
  float zoom;
};

cam3d

In SYSMEM, there are 4 memory-mapped 3D cameras that can be used for configuring the view.

To get a pointer to a 3D camera, use the getCam3D() function.

cam3d* cam = getCam3D(0); // get a pointer to 3D camera 0

To configure the view to a built-in 3D camera, use the camera3D() function.

camera3D(0); // use 3D camera 0

Note

When camera3D() is called, depth testing is automatically enabled.

The cam3d struct has the following definition:

struct cam3d {
  vec3 position;
  vec3 target;
  vec3 up;
  float fov;
  int projection;
};

light

In SYSMEM, there are 8 memory-mapped lights that are used when lighting is enabled.

To get a pointer to a light, use the getLight() function.

light* light0 = getLight(0); // get a pointer to light 0

The light struct has the following definition:

struct light {
  int enabled;
  int type;
  float radius;
  int color;
  vec3 position;
  vec3 direction;
};

Collision Objects

Warning

The physics system is very experimental and subject to change.

Currently, collision detection is only implemented for colaabb vs colaabb.

ERA-3D has built-in API functions for 3D collision detection. These API functions accept pointers to collision object structs to know what objects to test.

Each collision object struct contains a type integer member that must be set to the correct ID value in order for the collision functions to properly identify them.

The following table lists all collision object IDs:

ID Collision Object Struct
0 colpoint
1 colaabb
2 colsphere
3 colcylinder
4 coltriangle

To check for collisions using these structs, use the checkCollision() API function.

colaabb box1 = colaabb(1, vec3(), 1.0, 0.5, 1.0);
colaabb box2 = colaabb(1, vec3(), 0.5, 1.0, 0.5);

if (checkCollision(&box1, &box2)) {
  // box1 and box2 are colliding!
  // handle collision here...
}

colpoint

A colpoint represents a single point in 3D space.

struct colpoint {
  int type;
  vec3 position;
};

colaabb

A colaabb represents an Axis Aligned Bounding Box, a 3D box that cannot be rotated.

struct colaabb {
  int type;
  vec3 position;
  float width;
  float height;
  float depth;
};

colsphere

A colsphere represents a 3D sphere.

struct colsphere {
  int type;
  vec3 position;
  float radius;
};

colcylinder

A colcylinder represents a 3D cylinder.

struct colcylinder {
  int type;
  vec3 position;
  float height;
  float radius;
};

coltriangle

A coltriangle represents a 3D triangle.

struct coltriangle {
  int type;
  vec3 point1;
  vec3 point2;
  vec3 point3;
};

Differences From C

The following table lists the main differences between C and ERA-C semantics.

C ERA-C
Multi-line comments cannot be nested Multi-line comments can be nested
Multiple integer and floating-point types of various sizes Only int and float, both 32-bit (4 bytes)
Leading zeros indicate octal format Leading zeros do not indicate octal format, and are ignored
Pointer <-> Integer conversion is not generally recommended Pointer <-> int is 100% legal
NULL null
NULL == 0 null != 0
void foo() accepts any number of arguments void foo() does not accept any arguments
void foo(void) does not accept any arguments void foo(void) is a compile error
main() is the program starting point Hook functions init(), update(), and draw() are called periodically