Guia da API Weaver para Desenvolvimento de Jogos
Guia da API Weaver para Desenvolvimento de Jogos
1. Introdu~ao
1.1. Organização de Arquivos
Quando um usuário digita weaver PROJETO na linha de comando, um diretório
com um novo projeto Weaver e criado. Dentro deste diretório, o arquivo que contém
o loop principal está em src/game.c e nele encontramos:
#include "game.h"
int main(void){
Winit(); // Initializes Weaver
Wloop(main_loop); // Enter a new game loop
return 0;
}
E dentro de src/game.h, nós encontramos:
1
#ifndef _game_h_
#define _game_h_
#include "weaver/weaver.h"
#include "includes.h"
struct _game_struct{
// Voc^
e pode personalizar esta estrutura declarando variáveis aqui.
// Mas n~
ao mude seu nome. E acesse ela por meioda variável [Link]
int whatever; // <- Variável só pra prevenir erro em certo compilador
} _game;
void main_loop(void);
#endif
Notar que neste arquivo existe uma estrutura que pode ser personalizada pelo usuário e cumpre
o papel de centralizar algumas variáveis com estados globais. O tipo de coisa que para um jogo deve
ser preservada ao salvarmos o progresso de um jogador. Ou que deve ser acessı́vel para plugins.
Ou ainda que deve ser transmitida para informar o estado do jogo a clientes conectados à rede.
Segundo o comentário acima, esta estrutura deve ser referenciada pela variável [Link], o que
já nos indica que a API será organizada de modo a fornecer um struct chamado W onde serão
centralizados os recursos da API.
O arquivo includes.h é apenas um cabeçalho que inclui em um projeto todos os cabeçalho
de módulos criados pelo usuário (cada módulo é um arquivo de código C e um cabeçalho).
Todo o código da API deve então estar presente ou ser incluı́da por macros dentro dos arquivos
weaver.c e weaver.h. A organização do weaver.h é:
Arquivo: project/src/weaver/weaver.h:
#ifndef _weaver_h_
#define _weaver_h_
#ifdef __cplusplus
extern "C" {
#endif
#include "../../conf/conf.h"
#include <stdlib.h> // Always useful. Declares ’size_t’.
<Seção a ser Inserida: Estrutura Global>
<Seção a ser Inserida: Cabeçalhos Weaver>
<Seção a ser Inserida: Macros Weaver>
#ifdef __cplusplus
}
#endif
#endif
Notar que incluı́mos no cabeçalho o arquivo de configuração conf.h responsável
por controlar e configurar o comportamento de nosso motor.
1.2. A Estrutura W
A tal “estrutura global” referenciada nos códigos acima é o struct chamado
W. Já mencionamos no comentário que colocaremos nele o struct game struct
game que definimos em game.h. Podemos então começar a definir tal estrutura:
2
Seção: Estrutura Global:
// Esta estrutura conterá todas as variáveis e funç~
oes definidas pela
// API Weaver:
extern struct _weaver_struct{
struct _game_struct *game;
<Seção a ser Inserida: Variáveis Weaver>
<Seção a ser Inserida: Funções Weaver>
} W;
Notar que além de [Link], existirão outras variáveis presentes dentro desta
estrutura. Basicamente iremos centralizar dentro dela todas as funções públicas da
nossa API. Só não estarão nela funções que começam com “ ”, e que são consideradas
internas e não deveriam ser usadas por programadores utilizando a API. Desta forma
deixamos bem delimitado o que faz parte da API e também evitamos poluir com
nomes o “namespace” global de programas em C.
Também definiremos aqui a estrutura geral de nosso arquivo weaver.c:
Arquivo: project/src/weaver/weaver.c:
#include "weaver.h"
#include "../game.h"
<Seção a ser Inserida: API Weaver: Cabeçalhos Internos>
<Seção a ser Inserida: API Weaver: Variáves Estáticas>
<Seção a ser Inserida: API Weaver: Definições>
<Seção a ser Inserida: API Weaver: Funções>
<Seção a ser Inserida: API Weaver: Base>
Nas definições declaramos novos tipos de estruturas de dados que forem neces-
sárias. A primeira coisa que já podemos definir é a estrutura W, a qual é apenas
declarada no cabeçalho:
Seção: API Weaver: Definições:
struct _weaver_struct W;
Na parte de funções definimos as funções a serem usadas. Já a última partedo
arquivo contém as funções mais básicas da API. As únicas que não são colocadas
dentro da estrutura W. Elas são a função de inicialização e a função que encerra o
funcionamento do motor.
3
<Seção a ser Inserida: API Weaver: Finalização>
exit(0);
}
O uso da função exit nos obriga a inserir o cabeçalho:
Seção: Cabeçalhos Weaver:
#include <stdlib.h>
Os demais códigos que serão executados durante a inicialização e finalização
serão descritos ao longo do artigo.
2. Contagem de Tempo
Weaver mede o tempo em microssegundos (10−6 s) e armazena a sua contagem de
tempo em pelo menos 64 bits de memória. Weaver serve para criar programas que
executam dentro de um laço principal. Então além do tempo total em microssegun-
dos desde que o programa inicializou, também armazenamos a diferença de tempo
entre a iteração atual do programa e a última.
Então para começar devemos ter um lugar onde devemos armazenar a última
medida de tempo que fizemos. Usaremos para isso uma variável global. No Windows
usamos um dos tipos especı́ficos para representar inteiros grandes (e incluimos o
cabeçalho necessário para usá-lo) e nos demais sistemas usamos uma estrutura de
valor de tempo de alta resolução.
Seção: Cabeçalhos Weaver:
#if defined(_WIN32)
#include <windows.h>
LARGE_INTEGER _last_time;
#else
#include <sys/time.h>
struct timeval _last_time;
#endif
A ideia é que esta variável armazene sempre a última medida de tempo. Ela é
inicializada com a primeira medida de tempo na inicialização:
Seção: API Weaver: Inicialização:
#if defined(_WIN32)
QueryPerformanceCounter(&_last_time);
#else
gettimeofday(&_last_time, NULL);
#endif
Após a inicialização, todas as outras atualizações desta variável deverão ser feitas
por meio da função declarada abaixo:
Seção: Cabeçalhos Weaver (continuação):
unsigned long _update_time(void);
Tal função irá ler o tempo atual e armazenar na variável. Ela irá sempre retornar
a diferença de tempo em microssegundos entre a leitura atual e a última. Em
sistemas Unix faremos isso exatamente da maneira recomendada pelo manual da
GNU Glbc de modo a tornar a subtração de tempo mais portável e funcional mesmo
que os elementos da estrutura timeval sejam armazenadas como “unsigned”. A
desvantagem é que o código se torna menos claro. O código fica sendo:
Seção: API Weaver: Definições:
#if !defined(_WIN32)
unsigned long _update_time(void){
int nsec;
unsigned long result;
4
struct timeval _current_time;
gettimeofday(&_current_time, NULL);
// Aqui temos algo equivalente ao "vai um" do algoritmo da subtraç~
ao:
if(_current_time.tv_usec < _last_time.tv_usec){
nsec = (_last_time.tv_usec - _current_time.tv_usec) / 1000000 + 1;
_last_time.tv_usec -= 1000000 * nsec;
_last_time.tv_sec += nsec;
}
if(_current_time.tv_usec - _last_time.tv_usec > 1000000){
nsec = (_current_time.tv_usec - _last_time.tv_usec) / 1000000;
_last_time.tv_usec += 1000000 * nsec;
_last_time.tv_sec -= nsec;
}
if(_current_time.tv_sec < _last_time.tv_sec){
// Overflow
result = (_current_time.tv_sec - _last_time.tv_sec) * (-1000000);
// Sempre positivo:
result += (_current_time.tv_usec - _last_time.tv_usec);
}
else{
result = (_current_time.tv_sec - _last_time.tv_sec) * 1000000;
result += (_current_time.tv_usec - _last_time.tv_usec);
}
_last_time.tv_sec = _current_time.tv_sec;
_last_time.tv_usec = _current_time.tv_usec;
return result;
}
#endif
Em sistema Windows, já existe uma função que trata o tempo como sendo em
microssegundos exatamente no formato que já era usado antes mesmo de portarmos
Weaver para Windows na versão beta. Por causa disso, a função se torna muito
mais simples:
Seção: API Weaver: Definições:
#if defined(_WIN32)
unsigned long _update_time(void){
LARGE_INTEGER prev;
[Link] = _last_time.QuadPart;
QueryPerformanceCounter(&_last_time);
return (_last_time.QuadPart - [Link]);
}
#endif
5
if([Link][W ANY])
Wexit loop();
LOOP END: // Código executado na finalizaç~ ao
return;
}
Antes de entendermos como devemos entrar em um loop principal corretamente, é importante
descrever como este loop é executado. Nota-se que ele possui uma região de inicialização, de
execução e finalização demarcada por rótulos escritos em letras maiúsculas.
Interpretar isso é bastante simples. É perfeitamente possı́vel interpretar o código acima como:
for(;;)
nome do loop();
E dentro da função de loop principal nós não colocamos um laço explı́cito. Ao invés disso, nós
decidimos qual parte da função deve ser executada com ajuda dos rótulos inseridos, os quais na
verdade são macros com lógica adicional embutida junto a alguns comandos goto que decidem o
que será executado a cada vez que a função é chamada.
Uma consequência disso é que não é possı́vel lidar com variáveis declaradas dentro da inicia-
lização de um loop principal. Elas ,só teriam um valor correto dentro da inicialização, e dentro do
corpo do loop principal seu valor seria indefinido. Por exemplo, o seguinte código terá resultado
indefinido e talvez não imprima nada na tela:
// ERRADO
void loop(void){
LOOP INIT:
int var = 5;
LOOP BODY:
if(var == 5)
printf("var == 5\n");
LOOP END:
}
Já o seguinte código iria imprimir na saı́da padrão a cada iteração do loop:
// CERTO
static int var;
void loop(void){
LOOP INIT:
var = 5;
6
LOOP BODY:
if(var == 5)
printf("var == 5\n");
LOOP END:
}
Outra coisa que deve ser levada em conta é que as macros utilizadas escondem outro detalhe
importante: não é apenas um laço principal que executamos, mas existem dois simultâneos. Um
laço principal executa com uma frequência fixa: o laço que cuida da fı́sica e da lógica do jogo. Outro
laço, nós apenas fazemos executar o mais rápido que der no hardware atual: o laço responsável
por renderizar coisas na tela.
Idealmente para cada iteração do laço de fı́sica e lógica executamos uma ou mais iterações de
nosso laço de renderização. Isso significa que podemos renderizar com uma frequência maior que
executamos a iteração responsável por realmente mover objetos, detectar colisões e ler entrada do
usuário. Para que a cada vez nós não renderizemos exatamente a mesma imagem, o que derrotaria
o propósito de fazermos isso, nós interpolamos a posição dos objetos da tela de acordo com seus
valores de aceleração, velocidade e posição.
Garantindo que a nossa fı́sica e lógica do jogo execute sempre em intervalos constantes, nós
garantimos o determinismo necessário para podermos sincronizar partidas por meio de redes como
a Internet. E renderizando os objetos na tela o mais rápido que podemos com ajuda de interpolação
nos dá a experiência visual mais suave e natural que for possı́vel.
Uma referência e maiores detalhes de como implementar isso pode ser encontrado em [Fiedler
2004]. Nossa implementação será como mostrado na referência, com exceção de que nosso código
será muito menos transparente por ter que estar contido dentro de macros sem usar loops explı́citos.
Vamos agora definir nas subseções seguintes o que exatamente deverá existir em cada uma das
macros que devem estar presentes em todo laço principal.
7
uma tela de carregamento. Saber durante a execução o nome do laço em que esta-
mos é útil tanto para depuração como para podermos carregar recursos diferentes
dependendo do laço em que estamos. Por causa disso, ambas as variáveis devem ser
declaradas na estrutura W . O tamanho máximo de nome de um laço que podemos armazenar
pode ser personalizado com a macro W MAX LOOP NAME.
Seção: Variáveis Weaver (continuação):
// Dentro da estrutura W:
#if !defined(W_MAX_LOOP_NAME)
#define W_MAX_LOOP_NAME 64
#endif
unsigned pending_files;
char loop_name[W_MAX_LOOP_NAME];
Na inicialização ajustamos tais variáveis como 0 e NULL respectivamente:
Seção: API Weaver: Inicialização (continuação):
W.pending_files = 0;
W.loop_name[0] = ’\0’;
A função que usaremos para sair do laço será esta:
Seção: Cabeçalhos Weaver (continuação):
#if !defined(_MSC_VER)
void _exit_loop(void) __attribute__ ((noreturn));
#else
__declspec(noreturn) void _exit_loop(void);
#endif
Mas não iremos definir ela ainda. Pelo cabeçalho nota-se que é uma função que
nunca retorna, tendo isso especificado tanto pela convenção do GCC e Clang como
pela convenção do Visual Studio. Isso porque o que ela fará é apenas chamar o
código do laço anterior, ou sair do programa dependendo do caso.
Após descrever tudo isso, podemos enfim definir a macro de inı́cio de laço:
Seção: Cabeçalhos Weaver (continuação):
#define LOOP_INIT \
if(!_running_loop){ \
if(W.pending_files) \
return; \
if(!_loop_finalized){ \
_loop_finalized = true; \
goto _LOOP_FINALIZATION; \
} \
_exit_loop(); \
} \
if(!_loop_begin) \
goto _END_LOOP_INITIALIZATION; \
snprintf(W.loop_name, W_MAX_LOOP_NAME, "%s", __func__); \
_BEGIN_LOOP_INITIALIZATION
Terminamos com o identificador BEGIN LOOP INITIALIZATION, o qual será o
verdadeiro nome do rótulo que existirá por trás de nossa macro.
8
Ao chega nesta macro, devemos ajustar como falsa a informação de que é nossa
primeira execução do laço, pois assim não iremos executar a inicialização novamente.
Em seguida, aproveitamos para colocar um desvio incondicional por trás de um if
que garanta que ele nunca seja executado para o rótulo que termina a macro anterior.
Esse desvio nunca irá ocorrer, mas isso previne que o compilador reclame que o rótulo
que encerra a última macro não é usado. Em seguida, crimaos o verdadeiro rótulo
que marca o fim da inicialização e o começo da execução do corpo do laço. Neste
começo de corpo do laço nós medimos o tempo que passou desde o último laço e
armazenamos em um acumulador. Se este acumulador tiver um valor maior que o
perı́odo de tempo entre execuções do código de fı́sica e lógica, então executamos o
código presente no laço e o código associado à fı́sica. Caso contrário, só ignoramos
tudo e vamos para a finalização onde apenas renderizamos na tela. Caso tenha
passado um longo tempo entre cada iteração de laço, executamos mais de uma vez
o código do corpo do laço.
O acumulador que usamos para saber se devemos executar a lógica e a fı́sica do
jogo será chamado de lag. Declaramos ele globalmente:
Seção: Cabeçalhos Weaver (continuação):
unsigned long _lag;
E o inicializamos:
Seção: API Weaver: Inicialização (continuação):
_lag = 0;
Existirão variáveis que poderão ser lidas pelo usuário com informações de tempo. Uma delas
(W.t) conterá a quantidae de microssegundos desde que o jogo inicializou. Outra delas ([Link])
conterá o intervalo entre execuções do motor de fı́sica e da lógica do jogo. Ambas as variáveis
precisam ser declaradas na estrutura W:
Seção: Variáveis Weaver (continuação):
unsigned long long t;
unsigned long dt;
A primeira das variáveis, obviamente deve ser inicializada como zero. A segunda
deve ser inicializada como tendo o mesmo valor que uma macro W TIMESTEP que pode
ser definida pelo usuário para que assim ele controle a granularidade de execução
do código mais pesado do motor de fı́sica e lógica do jogo. Se esta macro não
for definida, iremos assumir um valor de 40000 microssegundos. Isso significa uma
freuência de 25 Hz de execução do motor de fı́sica (25 vezes por segundo).
Seção: API Weaver: Inicialização (continuação):
#if !defined(W_TIMESTEP)
#define W_TIMESTEP 40000
#endif
[Link] = W_TIMESTEP;
W.t = 0;
O código da engine de fı́sica e lógica interna do jogo deve ficar encapsulado em
uma função chamada update:
Seção: Cabeçalhos Weaver (continuação):
void _update(void);
Por hora ainda não iremos definir o código a ser executado nesta função:
Seção: API Weaver: Base (continuação):
void _update(void){
<Seção a ser Inserida: Código a executar todo loop>
9
}
Mas com as definições que fizemos já podemos definir a nossa macro que marca
o começo do código do laço principal:
Seção: Cabeçalhos Weaver (continuação):
#define LOOP_BODY \
_loop_begin = false; \
if(_loop_begin) \
goto _BEGIN_LOOP_INITIALIZATION; \
_END_LOOP_INITIALIZATION: \
_lag += _update_time(); \
while(_lag >= [Link]){ \
_update(); \
_LABEL_0
Notar que a macro acima abre um laço com um while, mas não o encerra. Ele
deverá ser encerrado pelo código inserido pela macro que delimita o fim do corpo
do laço principal. A qual também deverá decrementar a variável lag para que este
não seja um laço infinito.
10
3.4. Entrando em Laço Principal.
Frequentemente estaremos trocando de laços principais ao longo de um jogo.
Mas nem sempre isso significa uma substituição completa. Alguns laços principais
ocorrem dentro de outros laços principais. Por exemplo, quando acessamos um
menu de configurações durante um jogo. Ou quando em um RPG clássico por
turnos mudamos para o modo de combate após um encontro aleatório.
Podemos então formar uma pilha de laços principais, onde ao sairmos do último
laço voltamos para o que está empilhado imediatamente abaixo dele ao invés de
sairmos do jogo.
Sendo assim, existem duas formas de entrar em um laço principal. Uma delas,
por meio da função que definiremos chamada Wloop e a segunda por meio da W-
subloop. A primeira envolve substituirmos o laço principal atual por um novo. A
segunda enolve apenas criar um laço principal dentro do laço atual. A primeira é
algo que só poderemos fazer se não houverem arquivos pendentes sendo carregados
(possivelmente haverá uma tela de carregamento neste caso). Por isso apenas nos
asseguramos disso por mieo de um truque com macros:
Seção: Cabeçalhos Weaver (continuação):
#if !defined(_MSC_VER)
void _Wloop(void (*f)(void)) __attribute__ ((noreturn));
void Wsubloop(void (*f)(void)) __attribute__ ((noreturn));
#else
__declspec(noreturn) void _Wloop(void (*f)(void));
__declspec(noreturn) void Wsubloop(void (*f)(void));
#endif
#define Wloop(a) ((W.pending_files)?(false):(_Wloop(a)))
Nenhum dos dois tipos de função irá retornar jamais. Então especificamos isso
tanto na convenção de compiladores como Clang e GCC como na do Visual Studio.
Vamos precisar de uma pilha de laços principais, que representaremos por meio
de uma sequência de ponteiros para funções. Essa nossa sequência será um vetor
com W MAX SUBLOOP posições. Esta macro poderá ser controlada pelo usuário, mas
se não estiver definida, assumiremos que será 3. Além da pilha, vamos precisar de
uma variável para nos informar em qual profundidade da pilha estamos no momento
( number of loops).
A declaração destas duas variáveis será:
Seção: Cabeçalhos Weaver (continuação):
#if !defined(W_MAX_SUBLOOP)
#define W_MAX_SUBLOOP 3
#endif
int _number_of_loops;
void (*_loop_stack[W_MAX_SUBLOOP]) (void);
E inicializamos a contagem do número de laços como zero:
Seção: API Weaver: Inicialização (continuação):
_number_of_loops = 0;
Entrar em um novo laço principal por meio de Wloop envolve verificar se já es-
tamos antes em um laço. Se for o caso, cancelamos ele. Em seguida, carregamos o
novo laço para a pilha e ajustamos o valor da contagem de laços em execução. Atu-
alizamos o nosso valor de contagem de tempo e finalmente executamos o laço. Em
ambiente Web Assembly em navegador de Internet isso envolve chamar diretamente
uma função que estabelece nossa função escolhida como laço principal. Nos demais,
basta executar a função em um while comum:
Seção: API Weaver: Base (continuação):
11
void _Wloop(void (*f)(void)){
if(_number_of_loops > 0){
<Seção a ser Inserida: Cancela Loop Principal>
_number_of_loops --;
}
<Seção a ser Inserida: Código antes de Loop e Subloop>
<Seção a ser Inserida: Código antes de Loop, não de Subloop>
_loop_stack[_number_of_loops] = f;
_number_of_loops ++;
#if defined(__EMSCRIPTEN__)
emscripten_set_main_loop(f, 0, 1);
#else
while(1)
f();
#endif
}
Cancelar um laço principal já existente envolve caso estejamos executando em
ambiente Web Assembly invocar a função que interrompe o laço atual:
Seção: Cancela Loop Principal:
#if defined(__EMSCRIPTEN__)
emscripten_cancel_main_loop();
#endif
Iniciar um novo subloop, ou sublaço, é bastante semelhante. Mas tratamos
de maneira diferente o nosso contador de laços, já que ele realmente precisa ser
incrementado. E também temos que checar se tivemos um estouro na nossa pilha
de laços:
Seção: API Weaver: Definições (continuação):
void Wsubloop(void (*f)(void)){
#if defined(__EMSCRIPTEN__)
emscripten_cancel_main_loop();
#endif
<Seção a ser Inserida: Código antes de Loop e Subloop>
<Seção a ser Inserida: Código antes de Subloop, não de Loop>
if(_number_of_loops >= W_MAX_SUBLOOP){
fprintf(stderr, "Error: Max number of subloops achieved.\n");
fprintf(stderr, "Please, increase W_MAX_SUBLOOP in conf/conf.h"
" to a value bigger than %d.\n", W_MAX_SUBLOOP);
exit(1);
}
_loop_stack[_number_of_loops] = f;
_number_of_loops ++;
#if defined(__EMSCRIPTEN__)
emscripten_set_main_loop(f, 0, 1);
#else
while(1)
f();
#endif
}
Vamos incluir o cabçalho para podermos imprimir mensagens de erro:
Seção: Cabeçalhos Weaver (continuação):
12
#include <stdio.h>
Uma coisa que faremos tanto antes de um laço como de um sublaço é atualizar a variável
running loop que avisa ao motor que realmente estamos executando um laço ao invés de
tentar sair dele, ajustamos a variável que diz que estamos entrando no começo de um laço e que
portanto precisaremos executar o código de inicialização, a variável que informa que a finalização
do laço ainda não foi executada. Estas são as variáveis que controlam o fluxo de execução do laço.
Também atualizamos nosso contador de tempo:
Seção: Código antes de Loop e Subloop:
_running_loop = true;
_loop_begin = true;
_loop_finalized = false;
_update_time();
13
_loop_stack[_number_of_loops - 1]();
#endif
}
}
14
Como salvar o estado atual requer escrever na memória, vamos precisar saber
qual o alinhamento de bytes recomendado para o nosso CPU. Weaver obtém este
valor da macro W MEMORY ALIGNMENT que pode ser definida em conf/conf.h. Se ela
não estiver definida, nós usamos por padrão o tamanho de um unsigned long como
palpite.
Seção: Cabeçalhos Weaver (continuação):
#ifndef W_MEMORY_ALIGNMENT
#define W_MEMORY_ALIGNMENT (sizeof(unsigned long))
#endif
Com isso podemos salvar o estado da pilha esquerda e direita de nossa arena de
memória:
Seção: Código antes de Loop e Subloop (continuação):
_Wmempoint(memory_arena, W_MEMORY_ALIGNMENT, 0);
_Wmempoint(memory_arena, W_MEMORY_ALIGNMENT, 1);
Isso significa que assim que sairmos de um laço, devemos restaurar o estado
da arena de memória para como estava quando entramos nele, tanto na pilha de
memória da esquerda como da direita:
Seção: Cancela Loop Principal (continuação):
_Wtrash(memory_arena, 0);
_Wtrash(memory_arena, 1);
E isso basicamente será o coletor de lixo de nosso projeto. Vamos agora definir
uma função para que o usuário possa usar nossa função de alocação de memória.
Todas as alocações feitas pelo usuário devem ser da pilha esquerda da memória,
usando o alinhamento padrão:
Seção: API Weaver: Funções:
static void *_alloc(size_t size){
return _Walloc(memory_arena, W_MEMORY_ALIGNMENT, 0, size);
}
Vamos declarar um ponteiro para esta função dentro da estrutura W para que a
função possa ser invocada como [Link]():
Seção: Funções Weaver:
void *(*alloc)(size_t);
E a função estará pronta para ser usada após a inicialização:
Seção: API Weaver: Inicialização (continuação):
[Link] = _alloc;
De ni~oes Vazias
As seguintes definições ainda estão em branco e estão aqui para manter o projeto
compilando com sucesso. Esta parte não existirá na versão final deste documento
e as partes que aparecem aqui vazias estarão posteriormente preenchidas e melhor
explicadas.
Seção: Código antes de Subloop, não de Loop:
Seção: Código após sairmos de Subloop:
Seção: Código a executar todo loop:
Seção: Código de renderização:
15
Seção: Código antes de Loop, não de Subloop:
Seção: Macros Weaver:
Refer^en ias
Fiedler, G. (2004) “Fix Your Timestep!”, acessado em 2020 em:“[Link]
16