O TEMA DO FÓRUM ESTÁ EM MANUTENÇÃO. FEEDBACKS AQUI: ACESSAR

Programação de Eventos 101

Iniciado por Brandt, 07/02/2021 às 03:16

07/02/2021 às 03:16 Última edição: 11/02/2021 às 14:06 por Brandt
Programação de Eventos 101

"Banner"

Pré-requisitos:

  • RPG Maker VX Ace instalado
  • Conhecimento básico de matemática (i.e. aritmética básica e aritmética modular/"do relógio")
  • Nenhum conhecimento de scripts
1 Introdução
Essa aula tem como objetivo ensinar programação por eventos no RPG Maker. A intenção é ser uma aula introdutória, então não usaremos Script Calls, que são comandos avançados.

Para isso, vamos usar a versão mais apropriada do RPG Maker para esse objetivo: o RPG Maker VX Ace. Note que esse tutorial não funciona com as versões menos avançadas da engine, como o RPG Maker 2000, RPG Maker 2k3, RPG Maker MV ou RPG Maker MZ. As versões VX e XP servem, mas são ultrapassadas então desconsideramos ambas neste tutorial.

2 Anatomia de um Evento
"Anatomia"

O editor de eventos do RPG Maker é bastante simples, sendo composto por apenas 9 seções, contendo 13 dropdowns, 10 checkboxes e 9 botões, além de uma área de entrada de texto, uma lista interativa para os comandos e uma área de seleção de gráfico.

Para fins deste tutorial, vamos nos concentrar nas regiões destacadas na imagem:

2.1 Seleção de gráfico
Esta seção permite escolher o gráfico do evento. É bem maneiro.

"Seleção de Gráfico"

2.2 Ativador
O ativador determina quando os comandos do evento são executados.

"Ativador"

Existem cinco tipos de ativador:


  • Botão de ação: Ativa o evento quando o jogador pressiona o botão de interação olhando para o evento
  • Tocar Jogador: Ativa o evento quando ele toca o jogador
  • Tocar Evento: Ativa o evento quando ele toca outro evento, não incluindo a si mesmo
  • Executar Automático: Executa o evento em loop e trava o jogo. Não é útil, porque trava o jogo
  • Processo Paralelo: Executa o evento em loop e não trava o jogo
Neste tutorial, usaremos apenas o Processo Paralelo, de forma a não confundir o leitor com as formas de ativação diversas, que não são nosso foco.

2.3 Conteúdo
Esta seção é onde inserimos os comandos de fato. Ao clicar duas vezes em uma linha da lista, vemos a seguinte janela:

"Comandos"

E quando inserimos um comando ele fica visível na janela, desta forma:

"Lista"

Note que a forma como ele exibe o comando não é nem um pouco críptica e/ou remanescente de linhas de comando primitivas, além de ser muito mais legível que código equivalente (como, por exemplo, message 'Hello World!'). Isso torna programar com eventos mais fácil que programar com scripts.

2.4 Condições
Por último mas não menos importante, temos a seção de condições do evento. As condições determinam condições de ativação adicionais para a execução do evento, como switches e valores de variáveis.

"Lista"

3 Variáveis
Variáveis são a parte mais fundamental da programação por eventos. Por isso, é importante entender elas profundamente antes de começarmos.

Algumas pessoas vão te dizer que as variáveis nos eventos são a mesma coisa que as variáveis na matemática, mas isso além de não explicar muita coisa (quase ninguém entende direito as variáveis na matemática), é um pouco enganoso.

Na matemática (em especial na lógica), comumente, existem dois tipos de variável: variáveis livres e variáveis ligadas (Ler mais). Uma variável livre funciona como uma lacuna a ser preenchida, e só faz sentido no contexto de uma função ou predicado (por exemplo, na função x^2 + x, x é variável livre). Uma variável ligada, por outro lado, sempre tem uma condição associada e pode aparecer em uma expressão isolada (por exemplo, em ∀x,p,q (∃m (p = mq) ∧ x|p -> x|m ∧ x|q), x, p, q e m são variáveis ligadas).

Mas o que isso tem a ver com eventos? Bom, não muito. Na prática, é muito difícil pensar em eventos como funções ou objetos matemáticos similares, então a analogia das variáveis deixa a desejar.

Ao invés disso, podemos pensar em variáveis como "caixas", onde cada caixa pode conter um número. As caixas são independentes, e eu posso tirar e colocar o que quiser nelas; além disso, se quiser, posso combinar o conteúdo de duas caixas (e.g. somar uma em outra, subtrair, dividir, etc.)

"Variáveis"

3.1 Operações com variáveis
O RPG Maker permite algumas operações com as variáveis, visíveis na janela do comando "Controle de Variáveis" (primeira aba, segundo item na seção "Progressão do Jogo").

"Controle de variáveis"

Neste tutorial, usaremos as seis operações disponíveis, mas nos limitaremos aos operandos "Constante" e "Variável", visto que os outros três (principalmente o Script) são muito avançados.

Todas as operações devem ser relativamente autoexplicativas, exceto talvez a de "Resto". O "Resto" executa a operação de resto da divisão na variável, permitindo que façamos cálculos usando Aritmética Modular. Isso será crucial para programarmos com eventos! /o/

Um exemplo comum de aritmética modular são os ponteiros de um relógio: no relógio, temos marcas de 1 a 12, e quando somamos valores maiores que 12 (ou menores que 1) o ponteiro "dá a volta". Por exemplo, no relógio, 5 + 10 ≡ 3, 12 + 1 ≡ 1, x + 12 ≡ x, etc.; dizemos então que 5 + 10 ≡ 3 (mod 12), 12 + 1 ≡ 1 (mod 12), e assim por diante. Em geral, n + x ≡ x (mod n).

4 Condições
Condições controlam o fluxo de execução do evento. Você pode por exemplo condicionar uma mensagem à ativação de uma switch:

"Condição de Switch"

E o evento ficaria assim:

"Condição de Switch (Código)"

Além disso, é possível definir condições sobre variáveis, usando alguns comparadores:

"Condição de Variável"

E assim como no comando de controle, podemos comparar com uma constante ou com outra variável:

"Condição de Variável"

Existem outras abas e outra condições para esse comando, mas eles são mais avançados, e por isso não os usaremos nesse tutorial.

5 Loops
Um loop executa repetidamente os comandos dentro de um bloco. No editor de eventos, o loop se parece com isso:

"Loop"

O nome "Loop" vem da palavra inglesa para "ciclo", que tem a ver com o fluxo de execução do evento quando o desenhamos:

"Loop"

Claro, um loop pode ser um problema se não sairmos dele (caso contrário todo o resto do evento abaixo do loop não seria executao!). Para isso, usamos o comando "Sair do loop", geralmente acompanhado de uma condição:

"Sair do loop"

O exemplo acima conta até 10 com a variável "Exemplo" no loop, e então sai do Loop.

6 Bases numéricas
Uma base numérica (em inglês, radix) é uma instância de sistema numérico posicional. Se você já leu algo sobre computadores provavelmente deve ter pelo menos ouvido falar do tal "binário", que é usado internamente pelos computadores e ocasionalmente na computação.

O sistema binário é conveniente para o computador porque é fácil de construir eletronicamente, mas também tem suas vantagens além disso; em particular, cada dígito num número representado no sistema binário deve ser zero ou um, ou, equivalentemente, "verdadeiro" ou "falso".

Além do sistema binário, existem inúmeros sistemas numéricos (literalmente, existe pelo menos um para cada número real, e o conjunto dos reais é incontável), entre eles o decimal, que usamos normalmente, o hexadecimal que também é comum na programação, por ser relativamente fácil de traduzir para binário e ser mais compacto que o decimal, e o hexapentecontadiacosial (base 256), representado por uma string ASCII, com caracteres (dígitos) de 0 a 255.

6.1 S͉i͕s͈t͉ȩm̳a̪ Ḥe͈x̖a̻p̻ḙn̜t̙e̻c̱o͉n̼t͢a̪d̻i̫a̝c᷊ǫs͖i̼a̮l̗
Ok, o nome não existe, mas a construção é a mesma usada em "hexadecimal", então a partir de agora está valendo.

Este sistema numérico representa uma sequência de números de 0 a 255, e é útil em programação porque cada dígito do número representa um byte, equivalente a 8 bits (não coincidentemente, 2⁸ = 256), que é uma unidade de organização de bits extremamente comum em computadores.

Nos eventos do RPG Maker temos acesso apenas ao sistema numérico decimal, mas podemos converter entre as bases de forma relativamente simples. O artigo da Wikipedia sobre conversões de bases numéricas tem uma explicação detalhada sobre conversões entre bases quaisquer.

A ideia geral é que o n-ésimo dígito de um número em base x representa o número de vezes que o valor xⁿ deve ser adicionado ao valor final; por exemplo, na base decimal: o número 1234 tem o primeiro dígito (0-ésimo) igual a 4, o segundo ("1-ésimo") igual a 3, e assim por diante, logo o valor do número é 4×10⁰ + 3×10¹ + 2×10² + 1×10³ = 4 + 30 + 200 + 1000 = 1234 (claro).

Seguindo a mesma lógica, o número "1234" em base 256 seria equivalente a 4×256⁰ + 3×256¹ + 2×256² + 1×256³ = (1690906)₁₀.

Mas para representar números na base 256 não bastam os 10 dígitos usuais (0-9), nem mesmo os 16 extendidos usados no hexadecimal (0-9, a-f). Para dar conta de todas as possibilidades, representaremos um único dígito na base 256 usando pares de dígitos hexadecimais, separando cada dígito com espaços por clareza. Por exemplo, o número 238×256⁰ + 255×256¹ + 192×256² seria representado como (C0 FF EE)₂₅₆. Ao mesmo tempo, podemos converter esse valor em decimal (e vice versa), calculando seu valor como usual: 238×256⁰ + 255×256¹ + 192×256² = 238 + 255×256 + 192×65536 = (12648430)₁₀.

Podemos usar a Calculadora de Programador do Windows™ para calcular esses valores, e convenientemente fazer as conversões para binário e hexadecimal:

"Calculadora de Programador do Windows™"

Com esse exemplo em mãos, podemos também analisar como fazemos para "extrair" um único dígito de um número numa base qualquer. De forma geral, o n-ésimo (contando do 0) dígito D de um número X na base B segue a seguinte igualdade: D ≡ ⌊X / Bⁿ⌋ (mod B), isto é, D é o resto da divisão de X / Bⁿ (arredondando para baixo) por B.

Na base 10, isso quer dizer por exemplo que o 2º dígito (1-ésimo) de 1234 é ⌊1234 / 10¹⌋ (mod 10) ≡ 123 (mod 10) ≡ 12×10 + 3 (mod 10) ≡ 3 (mod 10).

Equivalentemente, na base 256, isso quer dizer que o 1º dígito (0-ésimo) de (C0 FF EE)₂₅₆ é ⌊(C0 FF EE)₂₅₆ / 1⌋ (mod 256) ≡ (C0 FF EE)₂₅₆ (mod 256) ≡ (EE)₂₅₆ (mod 256).

De forma resumida, dividir um número pela base arredondando para baixo "descarta" o 0-ésimo dígito e tirar o resto descarta todos, exceto o 0-ésimo. Aplicando essas operações em sucessão (o expoente na base representa divisão repetida), conseguimos isolar um dígito arbitrário em um número naquela base.

Com isso, temos uma forma de obter e definir um dígito específico em um número representado em uma base numérica qualquer!

7 M̘̘͞͞e̫̫̿m̝̝︣︣ó̟̆̆r̠̠ͮͮi͈͈̔a̜̋̋ R̄͟͟Ǎ̙̙̌M̰̰᷀
O "RAM" na memória RAM do computador é uma sigla, do inglês Random Access Memory. O "Random" no nome quer dizer que a memória pode ser acessada em qualquer posição que se deseje. Isso é extremamente útil em computadores, porque permite que programas reservem áreas de memória e as acessem (para escrita e leitura) conforme precisarem.

Todo programa no computador usa memória RAM, e se vamos programar com eventos vamos precisar de algo equivalente.

Aí entra a nossa base recém-descoberta, a hexapentecontadiacosial. Na prática, a memória RAM do computador não passa de um único número (bem grande) na base 256. Cada byte na memória é um dígito, e um acesso na posição N na memória é equivalente a isolar o N-ésimo dígito desse numéro; escrever um byte na memória também é relativamente simples usando operações aritméticas:

# RAM = número correspondente à RAM
# P = posição na memória
# V = valor para escrever (0-255)

escrita(RAM, P, V) = RAM - (⌊RAM / 256ᴾ⌋ (mod 256)) + V×256ᴾ


Sendo assim, é bem possível simular a memória RAM usando uma única variável no RPG Maker! Aqui entra a superioridade da versão VX Ace da engine, visto que nela os eventos rodam em cima de Ruby, que tem suporte a Bignums por padrão. Em versões menos avançadas da engine, o número perderia suas propriedades a partir de certo valor. Por exemplo, no 2k ele provavelmente "daria a volta" e ficaria negativo, por motivos de Complemento para dois, e no MV ele provavelmente viraria um número de ponto flutuante, perdendo precisão e consequentemente informação. Sim, Ruby > All.

8 A̷̮ͣr̴̡͖̤᷊͓̈́q̷̡̏︡᷈͜ư̴̢̻̩͇ͮȋ̵͉̲́ͫ̎̕t̶͔̖̜ͮ᷇̾᷄͜͟e̴̢̝̱̓́t̷̟̩͉̳̜︢̋︠᷅u̵͚͗᷄ŗ̸͈͇͌̀ͪa̴͎̜̯͎︡͢ Ą̵̪̼̭̝͋ͥ͘R̶͖̭̩̤̤͆̈ͧ︣M̵̰̤͓͔̆v̴͓̦̝͉ͨ͌ͣͩ͒6̴̢̭᷿̎᷾︡̉͢-̴̭̰̜͔᷀́͆ͨ͢M̶᷊͔̩̠̏͜
Além de memória, todo computador tem um processador, que é o que de fato faz o trabalho de, você adivinhou, processar as coisas.

Mas o processamento não é mágica, e o processador precisa de uma forma de saber o que exatamente ele deve processar e como. Para isso, existem conjuntos de instruções, que fazem a interface entre os programas (software) e o hardware que os executa.

Para este tutorial introdutório, é interessante mantermos a complexidade ao mínimo. Para tanto, implementaremos uma arquitetura simples, com um conjunto de instruções reduzido.

Felizmente, existem as arquiteturas RISC, que são exatamente isso (a sigla significa Reduced Instruction Set Computer, ou "Computador com um conjunto reduzido de instruções"). Em especial, a arquitetura ARM (de Advanced RISC Machine) é muito bem documentada, e as especificações de diversas versões da arquitetura estão disponíveis gratuitamente no site da empresa.

Em particular, usaremos uma versão simplificada da já simples arquitetura Armv6-M, usada no processador Cortex M0 da ARM.

A implementação é relativamente simples, e usa apenas as técnicas descritas até agora. Para economizar tempo, você pode baixar a implementação pronta: Download do ZIP via Dropbox.

Essa implementação inclui apenas os eventos que simulam o processador, nenhum outro dado do projeto foi alterado.

8.1 C̸̪͚͍̿̒o̴͚̞̟̐͐m̸̡̻̝͐̐p̸̺̦͙̒͝ḯ̵͖̼͐͝l̴͕̠̟̔́͐a̴̞͕̔͘͝n̸̦͔͎̽̽d̵̫͉͍̾͘o̴̝̼̼̐́̚ p̵͔͖͕̿̈́̓a̵̝̠͎̾͒͝r̵̢̻͕͐̈́͋a̴̡͇͙̽̕͝ A̴̼͇̙͋͊͆R̴̝̠̙̐͌̈́M̸̼͎͚̚̕͝v̸̢̪͓̐͋̒6̴̫̞͔͑̔͝ c̸̦̘͉͌̀͝o̵̼̦͑̿͌m̴̡͎̀̈́̐͜ o̸̡͇͛̾͜͝ G̵̫͓̘͋͝͝C̸̝̙̘͌̈́̒C̵̡̘̦̾̒͝
Para que o processador faça algo, temos que escrever um programa com o que queremos, e então transformá-lo em um formato que o processador entenda.

Para isso, usaremos a linguagem C, e o toolchain de Arm do GCC. Deve ser possível fazer isso no Windows, mas é beem mais fácil no Linux; recomendo instalar o WSL com Ubuntu.

No Ubuntu, para instalar as ferramentas necessárias, basta executar os seguintes comandos:

Código: bash
sudo apt-get update -y
sudo apt-get install -y gcc-arm-none-eabi binutils-arm-none-eabi libc6-armel-cross libc6-dev-armel-cross


Feito isso, os executáveis serão instalados com os nomes usuais (gcc, objdump, objcopy, etc.), mais o prefixo arm-none-eabi- (e.g. arm-none-eabi-gcc, arm-none-eabi-ld, ...).

Para as seções seguintes, usaremos o seguinte programa de teste, que lê uma lista de números via entrada do usuário e ordena usando insertion sort:

Código: c
// arquivo: sample.c

#include <stdint.h>

#define RAM(offset) ((uint32_t*) (0x60000000 + (offset)))
#define STACK_BASE RAM(1024)
#define MEM(offset) (STACK_BASE + 4 * (1 + (offset)))

volatile uint32_t* peripheral = (uint32_t*) 0x40000000;

// Lê um word (32-bits) do dispositivo periférico mapeado (executa o comando
// de entrada numérica do evento e retorna o valor digitado)
inline static uint32_t input() {
    return *peripheral;
}

// Escreve um word (32-bits) para o dispositivo periférico mapeado (mostra uma
// mensagem com o número)
inline static void output(uint32_t word) {
    *peripheral = word;
}

uint32_t* elements = MEM(0);

// insertion sort
void sort(uint32_t* arr, uint32_t n) {
    for (int i = 1; i < n; i++) {
        uint32_t key = arr[i];

        int j;
        for (j = i - 1; j >= 0 && arr[j] > key; j--) {
            arr[j + 1] = arr[j];
        }

        arr[j + 1] = key;
    }
}

void _start() {
    uint32_t n = input();

    for (int i = 0; i < n; i++) {
        elements[i] = input();
    }

    sort(elements, n);

    for (int i = 0; i < n; i++) {
        output(elements[i]);
    }
}


Nota: O algoritmo insertion sort foi escolhido por ser eficiente com coleções pequenas.

8.2 D̴̡̺̕͠͠i̴͖͙̝̔͘͝s̵̟̞͎̿̚͝a̸͙̠͐͌͐s̸̢͇̈́̒͝s̴̢͍͎̈́̓͠é̵͉͉͖̾m̴̡͉͕̓̒͝b̵̙̘͓̒̕͝ĺ̸̪̠̝́͘ÿ̵̻̠͖́͝
Antes de mais nada, vamos compilar o programa usando a arquitetura do nosso processador:

Código: bash
arm-none-eabi-gcc -nostdlib -c -march=armv6 -mthumb sample.c


Essas flags indicam que o compilador deve usar a arquitetura armv6 com instruções thumb (subconjunto de 16-bits, suportado pelo Armv6-M), além de não incluir código gerado para a biblioteca padrão do C (-nostdlib).

A flag -c diz que queremos que o compilador gere um arquivo .o, que podemos usar para analisar o assembly gerado. Para isso, usamos o comando objdump:

Código: bash
arm-none-eabi-objdump -d sample.o --visualize-jumps


A saída deve ser algo assim:

sample.o:     file format elf32-littlearm


Disassembly of section .text:

00000000 <input>:
   0:   b580            push    {r7, lr}
   2:   af00            add     r7, sp, #0
   4:   4b02            ldr     r3, [pc, #8]    ; (10 <input+0x10>)
   6:   681b            ldr     r3, [r3, #0]
   8:   681b            ldr     r3, [r3, #0]
   a:   0018            movs    r0, r3
   c:   46bd            mov     sp, r7
   e:   bd80            pop     {r7, pc}
  10:   00000000        .word   0x00000000

00000014 <output>:
  14:   b580            push    {r7, lr}
  16:   b082            sub     sp, #8
  18:   af00            add     r7, sp, #0
  1a:   6078            str     r0, [r7, #4]
  1c:   4b03            ldr     r3, [pc, #12]   ; (2c <output+0x18>)
  1e:   681b            ldr     r3, [r3, #0]
  20:   687a            ldr     r2, [r7, #4]
  22:   601a            str     r2, [r3, #0]
  24:   46c0            nop                     ; (mov r8, r8)
  26:   46bd            mov     sp, r7
  28:   b002            add     sp, #8
  2a:   bd80            pop     {r7, pc}
  2c:   00000000        .word   0x00000000

00000030 <sort>:
  30:                b580       push    {r7, lr}
  32:                b086       sub     sp, #24
  34:                af00       add     r7, sp, #0
  36:                6078       str     r0, [r7, #4]
  38:                6039       str     r1, [r7, #0]
  3a:                2301       movs    r3, #1
  3c:                617b       str     r3, [r7, #20]
  3e:      /-------- e02c       b.n     9a <sort+0x6a>
  40:   /--|-------> 697b       ldr     r3, [r7, #20]
  42:   |  |         009b       lsls    r3, r3, #2
  44:   |  |         687a       ldr     r2, [r7, #4]
  46:   |  |         18d3       adds    r3, r2, r3
  48:   |  |         681b       ldr     r3, [r3, #0]
  4a:   |  |         60fb       str     r3, [r7, #12]
  4c:   |  |         697b       ldr     r3, [r7, #20]
  4e:   |  |         3b01       subs    r3, #1
  50:   |  |         613b       str     r3, [r7, #16]
  52:   |  |     /-- e00d       b.n     70 <sort+0x40>
  54:   |  |  /--|-> 693b       ldr     r3, [r7, #16]
  56:   |  |  |  |   009b       lsls    r3, r3, #2
  58:   |  |  |  |   687a       ldr     r2, [r7, #4]
  5a:   |  |  |  |   18d2       adds    r2, r2, r3
  5c:   |  |  |  |   693b       ldr     r3, [r7, #16]
  5e:   |  |  |  |   3301       adds    r3, #1
  60:   |  |  |  |   009b       lsls    r3, r3, #2
  62:   |  |  |  |   6879       ldr     r1, [r7, #4]
  64:   |  |  |  |   18cb       adds    r3, r1, r3
  66:   |  |  |  |   6812       ldr     r2, [r2, #0]
  68:   |  |  |  |   601a       str     r2, [r3, #0]
  6a:   |  |  |  |   693b       ldr     r3, [r7, #16]
  6c:   |  |  |  |   3b01       subs    r3, #1
  6e:   |  |  |  |   613b       str     r3, [r7, #16]
  70:   |  |  |  \-> 693b       ldr     r3, [r7, #16]
  72:   |  |  |      2b00       cmp     r3, #0
  74:   |  |  |  /-- db07       blt.n   86 <sort+0x56>
  76:   |  |  |  |   693b       ldr     r3, [r7, #16]
  78:   |  |  |  |   009b       lsls    r3, r3, #2
  7a:   |  |  |  |   687a       ldr     r2, [r7, #4]
  7c:   |  |  |  |   18d3       adds    r3, r2, r3
  7e:   |  |  |  |   681b       ldr     r3, [r3, #0]
  80:   |  |  |  |   68fa       ldr     r2, [r7, #12]
  82:   |  |  |  |   429a       cmp     r2, r3
  84:   |  |  \--|-- d3e6       bcc.n   54 <sort+0x24>
  86:   |  |     \-> 693b       ldr     r3, [r7, #16]
  88:   |  |         3301       adds    r3, #1
  8a:   |  |         009b       lsls    r3, r3, #2
  8c:   |  |         687a       ldr     r2, [r7, #4]
  8e:   |  |         18d3       adds    r3, r2, r3
  90:   |  |         68fa       ldr     r2, [r7, #12]
  92:   |  |         601a       str     r2, [r3, #0]
  94:   |  |         697b       ldr     r3, [r7, #20]
  96:   |  |         3301       adds    r3, #1
  98:   |  |         617b       str     r3, [r7, #20]
  9a:   |  \-------> 697b       ldr     r3, [r7, #20]
  9c:   |            683a       ldr     r2, [r7, #0]
  9e:   |            429a       cmp     r2, r3
  a0:   \----------- d8ce       bhi.n   40 <sort+0x10>
  a2:                46c0       nop                     ; (mov r8, r8)
  a4:                46c0       nop                     ; (mov r8, r8)
  a6:                46bd       mov     sp, r7
  a8:                b006       add     sp, #24
  aa:                bd80       pop     {r7, pc}

000000ac <_start>:
  ac:          b590             push    {r4, r7, lr}
  ae:          b085             sub     sp, #20
  b0:          af00             add     r7, sp, #0
  b2:          f7ff ffa5        bl      0 <input>
  b6:          0003             movs    r3, r0
  b8:          607b             str     r3, [r7, #4]
  ba:          2300             movs    r3, #0
  bc:          60fb             str     r3, [r7, #12]
  be:      /-- e00b             b.n     d8 <_start+0x2c>
  c0:   /--|-> 4b17             ldr     r3, [pc, #92]   ; (120 <_start+0x74>)
  c2:   |  |   681a             ldr     r2, [r3, #0]
  c4:   |  |   68fb             ldr     r3, [r7, #12]
  c6:   |  |   009b             lsls    r3, r3, #2
  c8:   |  |   18d4             adds    r4, r2, r3
  ca:   |  |   f7ff ff99        bl      0 <input>
  ce:   |  |   0003             movs    r3, r0
  d0:   |  |   6023             str     r3, [r4, #0]
  d2:   |  |   68fb             ldr     r3, [r7, #12]
  d4:   |  |   3301             adds    r3, #1
  d6:   |  |   60fb             str     r3, [r7, #12]
  d8:   |  \-> 68fb             ldr     r3, [r7, #12]
  da:   |      687a             ldr     r2, [r7, #4]
  dc:   |      429a             cmp     r2, r3
  de:   \----- d8ef             bhi.n   c0 <_start+0x14>
  e0:          4b0f             ldr     r3, [pc, #60]   ; (120 <_start+0x74>)
  e2:          681b             ldr     r3, [r3, #0]
  e4:          687a             ldr     r2, [r7, #4]
  e6:          0011             movs    r1, r2
  e8:          0018             movs    r0, r3
  ea:          f7ff fffe        bl      30 <sort>
  ee:          2300             movs    r3, #0
  f0:          60bb             str     r3, [r7, #8]
  f2:      /-- e00b             b.n     10c <_start+0x60>
  f4:   /--|-> 4b0a             ldr     r3, [pc, #40]   ; (120 <_start+0x74>)
  f6:   |  |   681a             ldr     r2, [r3, #0]
  f8:   |  |   68bb             ldr     r3, [r7, #8]
  fa:   |  |   009b             lsls    r3, r3, #2
  fc:   |  |   18d3             adds    r3, r2, r3
  fe:   |  |   681b             ldr     r3, [r3, #0]
 100:   |  |   0018             movs    r0, r3
 102:   |  |   f7ff ff87        bl      14 <output>
 106:   |  |   68bb             ldr     r3, [r7, #8]
 108:   |  |   3301             adds    r3, #1
 10a:   |  |   60bb             str     r3, [r7, #8]
 10c:   |  \-> 68bb             ldr     r3, [r7, #8]
 10e:   |      687a             ldr     r2, [r7, #4]
 110:   |      429a             cmp     r2, r3
 112:   \----- d8ef             bhi.n   f4 <_start+0x48>
 114:          46c0             nop                     ; (mov r8, r8)
 116:          46c0             nop                     ; (mov r8, r8)
 118:          46bd             mov     sp, r7
 11a:          b005             add     sp, #20
 11c:          bd90             pop     {r4, r7, pc}
 11e:          46c0             nop                     ; (mov r8, r8)
 120:          00000000         .word   0x00000000


Primeiro, o programa nos diz que o arquivo .o está no formato elf32-littlearm. Isso significa que ele segue a estrutura de um arquivo ELF (Executable and Linking Format) de 32-bits, codificado com little-endian e usando o conjunto de instruções da arquitetura ARM.

Em seguida, o programa mostra o disassembly da seção .text. Em arquivos ELF, o conteúdo é dividido em seções, cada qual tem seu propósito. A função da seção .text é, basicamente, conter as instruções executáveis do programa.

No disassembly da seção .text, o objdump divide o código ainda em duas partes: input, output, sort e _start, que não coincidentemente são os nomes das nossas funções.

Em cada parte, o programa mostra o código hexadecimal das instruções, seguido do código correspondente em linguagem de montagem.

No entanto, veja que as chamadas de função e acessos de memória estão zerados! Isso significa que o programa não sabe qual o endereço na memória das funções, e não sabe onde encontrar os dados do programa (que ficam numa seção separada, chamada .data). Isso é o trabalho do próximo componente na compilação, o linker, e veremos como adaptá-lo para que funcione da forma que precisamos.

8.3 ⷮ͊̽ʇ͑̒d̿͊̚ᴉ̽́͘ɹ̈́̀͝ɔ̓̓͝S͌̈́͌ ɹ̾̓̾ǝ̿̕̚ʞ̈́͐͘u̒͒͠ᴉ͑̐̚⅂̔͘ ǝ͊͘ ɐ̈́̔̚ᴉ͒̀͆ɹ͆͋͝o̗̐̒͠ɯ̈́͛͝ǝ̐̽ɯ̒̐͠ ǝ̽͝p͌̿ ö́̔̕ʇ͆́͝u͌͐̓ǝ̕͠ɯ̽͑͝ɐ̐̾͊ǝ̈́̈́͠d͌̾͑ɐ́̓͝ꟽ͐͋̕
Nosso processador foi organizado de forma a ter três seções principais de memória:


  • A ROM (Read-Only memory), que contém os dados do programa (código e dados estáticos)
  • A RAM, que já discutimos
  • Uma seção para "dispositivos periféricos", que no nosso caso fazem a interface do programa com o jogador, escrevendo uma mensagem ou pedindo um número através do comando "Inserir um Número..."
Por outro lado, o programa enxerga apenas uma "memória". Para acomodar esta separação, mapeamos os endereços de memória para cada uma das seções. Seguindo a especificação da arquitetura Armv6-M, a ROM usa os endereços 0x00000000 a 0x1FFFFFFF, os periféricos usam de 0x40000000 a 0x5FFFFFFF e a RAM usa os endereços 0x60000000 a 0x7FFFFFFF.

Obs.: A stack (pilha de chamada) usa os endereços 0x40000000 a 0x40000400, ocupando os primeiros 1Kb da RAM (256 words).

Por esse motivo, no nosso programa, definimos alguns ponteiros aparentemente arbitrários no começo:

Código: c
#define RAM(offset) ((uint32_t*) (0x60000000 + (offset)))
#define STACK_BASE RAM(1024)
#define MEM(offset) (STACK_BASE + 4 * (1 + (offset)))

volatile uint32_t* peripheral = (uint32_t*) 0x40000000;

// ...

uint32_t* elements = MEM(0);


Esses endereços correspondem ao mapeamento que fizemos da RAM e dos periféricos, e servem para informar o programa de onde ler/escrever cada informação. Em particular, a variável peripheral aponta para o primeiro endereço do dispositivo periférico (poderíamos usar qualquer endereço, a implementação ignora o offset), e a variável elements, responsável por armazenar os elementos da lista, aponta para o primeiro byte da RAM que não é ocupado pela stack.

Entretanto, mesmo que nós saibamos disso tudo, o compilador ainda não sabe. Para conseguirmos extrair o binário do executável que queremos usar, precisamos configurar o linker para levar esses endereços em conta, e aplicar o linker no objeto que compilamos.

A primeira parte, fazemos usando um script de linker:

/* arquivo linker.ls */

MEMORY {
    ROM(rx) : ORIGIN = 0x00000000, LENGTH = 512M
    RAM(rwx) : ORIGIN = 0x60000000, LENGTH = 512M
}

SECTIONS {
    . = ORIGIN(ROM);
    .text : {
        *(.text)
    } > ROM

    .data ALIGN(4) : {
        _data_start = .;
        *(.data)
        . = ALIGN(4);
        _data_end = .;
    } > ROM

    . = ORIGIN(RAM);
    .bss ALIGN(4) (NOLOAD) : {
        _bss_start = .;
        *(.bss)
        . = ALIGN(4);
        _bss_end = .;
    } > RAM

    _stacktop = ORIGIN(RAM) + 1024;
    _data_load = LOADADDR(.data);

    __bss_start__ = _bss_start;
    __bss_end__ = _bss_end;
}


O script acima "avisa" o linker que o programa será armazenado na ROM, que suporta somente leitura (rw) e começa no endereço 0x00000000, continuando por 512 Mb. Além disso, dizemos que a seção .data do programa também fica na ROM (geralmente, essa seção vai para a RAM! No nosso caso, porém, deixaremos ela na ROM junto do programa, por simplicidade).

Também especificamos que a RAM existe no endereço 0x60000000, pode ser escrita (rwx) e que a stack termina no endereço de origem da RAM + 1024 bytes.

Por fim, aplicamos o linker ao nosso arquivo de objeto, usando o script que criamos:

Código: bash
arm-none-eabi-ld -T linker.ls sample.o -o sample.elf


E temos o arquivo ELF completo! Agora quando usarmos o objdump veremos que as instruções de jump têm os endereços certo, e as leituras batem com a seção .data.

8.4 ⷮ͊̽o̪͇͖ᴉ̦̻͙ɹ͎̻͜ɐ̢̙͍̗u̫̝̦ᴉ̠̫͎q̞̟͖ ɐ̢̟̺ɹ̪̼͜ɐ͕͎̺d̡͔͓ ᖵ⅂͓̞͍Ǝ͍̝͜ o̦͙̝ʌ͍̘̦ᴉ̦͔n͚̞͎b͕͙̪ɹ̘̠̟∀̻̠̻
O último passo é converter o arquivo ELF em um binário que podemos jogar na RAM e deixar rodar. Para isso, vamos usar o comando objcopy, e depois o hexdump para visualizar os dados e copiar para o nosso evento. Por motivos de otimização, vamos recompilar o programa usando a flag -O2, para reduzir o tamanho do executável e melhorar o desempenho onde possível.

Primeiro, compilamos o programa:

Código: bash
arm-none-eabi-gcc -nostdlib -O2 -Wl,-Tlinker.ls -march=armv6 -mthumb sample.c -o sample.elf
[ICODE][/ICODE]

Fazemos os dois passos em um, passando as flags do linker através da flag -Wl do GCC.

Desta vez, o disassembly é muito mais enxuto:

$ arm-none-eabi-objdump -d sample.elf --visualize-jumps

sample.elf:     file format elf32-littlearm


Disassembly of section .text:

00000000 <sort>:
   0:                b5f0       push    {r4, r5, r6, r7, lr}
   2:                2901       cmp     r1, #1
   4:   /----------- d914       bls.n   30 <sort+0x30>
   6:   |            1f03       subs    r3, r0, #4
   8:   |            0089       lsls    r1, r1, #2
   a:   |            0005       movs    r5, r0
   c:   |            185f       adds    r7, r3, r1
   e:   |            2600       movs    r6, #0
  10:   |  /-------> 686c       ldr     r4, [r5, #4]
  12:   |  |         0032       movs    r2, r6
  14:   |  |         002b       movs    r3, r5
  16:   |  |     /-> 6819       ldr     r1, [r3, #0]
  18:   |  |     |   42a1       cmp     r1, r4
  1a:   |  |  /--|-- d90a       bls.n   32 <sort+0x32>
  1c:   |  |  |  |   6059       str     r1, [r3, #4]
  1e:   |  |  |  |   3b04       subs    r3, #4
  20:   |  |  |  |   3a01       subs    r2, #1
  22:   |  |  |  \-- d2f8       bcs.n   16 <sort+0x16>
  24:   |  |  |      0002       movs    r2, r0
  26:   |  |  |  /-> 3504       adds    r5, #4
  28:   |  |  |  |   3601       adds    r6, #1
  2a:   |  |  |  |   6014       str     r4, [r2, #0]
  2c:   |  |  |  |   42af       cmp     r7, r5
  2e:   |  \--|--|-- d1ef       bne.n   10 <sort+0x10>
  30:   \-----|--|-> bdf0       pop     {r4, r5, r6, r7, pc}
  32:         \--|-> 3201       adds    r2, #1
  34:            |   0092       lsls    r2, r2, #2
  36:            |   1882       adds    r2, r0, r2
  38:            \-- e7f5       b.n     26 <sort+0x26>
  3a:                46c0       nop                     ; (mov r8, r8)

0000003c <_start>:
  3c:          b5f8             push    {r3, r4, r5, r6, r7, lr}
  3e:          4f0d             ldr     r7, [pc, #52]   ; (74 <_start+0x38>)
  40:          683d             ldr     r5, [r7, #0]
  42:          6878             ldr     r0, [r7, #4]
  44:          6829             ldr     r1, [r5, #0]
  46:          2900             cmp     r1, #0
  48:   /----- d00f             beq.n   6a <_start+0x2e>
  4a:   |      008c             lsls    r4, r1, #2
  4c:   |      0003             movs    r3, r0
  4e:   |      1826             adds    r6, r4, r0
  50:   |  /-> 682a             ldr     r2, [r5, #0]
  52:   |  |   c304             stmia   r3!, {r2}
  54:   |  |   429e             cmp     r6, r3
  56:   |  \-- d1fb             bne.n   50 <_start+0x14>
  58:   |      f7ff ffd2        bl      0 <sort>
  5c:   |      cf09             ldmia   r7!, {r0, r3}
  5e:   |      18e4             adds    r4, r4, r3
  60:   |  /-> cb04             ldmia   r3!, {r2}
  62:   |  |   6002             str     r2, [r0, #0]
  64:   |  |   42a3             cmp     r3, r4
  66:   |  \-- d1fb             bne.n   60 <_start+0x24>
  68:   |  /-> bdf8             pop     {r3, r4, r5, r6, r7, pc}
  6a:   \--|-> 2100             movs    r1, #0
  6c:      |   f7ff ffc8        bl      0 <sort>
  70:      \-- e7fa             b.n     68 <_start+0x2c>
  72:          46c0             nop                     ; (mov r8, r8)
  74:          00000078         .word   0x00000078


Então, executamos o comando objcopy para exportar os dados binários do programa, seguido de um hexdump para visualizarmos os dados:

$ arm-none-eabi-objcopy -O binary sample.elf sample.bin
$ hexdump -C sample.bin
00000000  f0 b5 01 29 14 d9 03 1f  89 00 05 00 5f 18 00 26  |...)........_..&|
00000010  6c 68 32 00 2b 00 19 68  a1 42 0a d9 59 60 04 3b  |lh2.+..h.B..Y`.;|
00000020  01 3a f8 d2 02 00 04 35  01 36 14 60 af 42 ef d1  |.:.....5.6.`.B..|
00000030  f0 bd 01 32 92 00 82 18  f5 e7 c0 46 f8 b5 0d 4f  |...2.......F...O|
00000040  3d 68 78 68 29 68 00 29  0f d0 8c 00 03 00 26 18  |=hxh)h.)......&.|
00000050  2a 68 04 c3 9e 42 fb d1  ff f7 d2 ff 09 cf e4 18  |*h...B..........|
00000060  04 cb 02 60 a3 42 fb d1  f8 bd 00 21 ff f7 c8 ff  |...`.B.....!....|
00000070  fa e7 c0 46 78 00 00 00  00 00 00 40 10 04 00 60  |...Fx......@...`|
00000080


Ora, convenientemente, o hexdump escreve os dados em base hexapentecontadiacosial! Podemos então escrever o número da ROM correspondente ao programa dígito a dígito, usando o evento comum [(ROM) Write Byte] disponível na implementação disponibilizada para download:

"Evento"

Ao fim, definimos o endereço de início no registrador PC (Program Counter). No nosso caso a função _start fica no endereço 3C, ou 60; o PC é adiantado por 4 bytes, então o definimos para 64.

"Evento"

Ativamos também a switch local para iniciar o processamento do programa em paralelo:

"Evento"

Note que o evento responde a um sinal de HALT para terminar a execução. Isso acontece quando o PC volta para 0, indicando que a função _start retornou.

E pronto! Temos um processador de 60Hz rodando, capaz de ordenar uma lista de números digitada pelo usuário.

Baixe a versão completa com o evento funcionando pelo Dropbox.

9 Conclusão

Claramente, eventos > scripts, e RPG Maker VX Ace > All.

Fora isso, esse trabalho serviu como uma prova de conceito bem interessante, e envolveu bastante estudo sobre o funcionamento de um processador no nível mais fundamental (sem a eletrônica claro, porque é chato).

Pessoalmente, pra ser sincero, fiquei surpreso de ter conseguido compilar um programa em C com o GCC e rodar usando só eventos no RPG Maker, e teve bastante novidade no processo (tipo, linker script, wtf?).

E por tudo que é sagrado, parem de fazer menus por evento. Façam computadores.

Para quem tiver interesse também, uma das fontes de inspiração para essa empreitada foi a playlist do Ben Eater ensinando a criar um computador 8-bit em uma breadboard. É um conteúdo bem maneiro, recomendo bastante pra qualquer um que tiver interesse nesse tipo de coisa.
~ Masked

07/02/2021 às 11:13 #1 Última edição: 07/02/2021 às 11:14 por Kerazzk
Amei o shitpost. Maaas

Mais um programador falando pra não fazermos coisas por eventos  :coffee:
Se a gente faz menus, ABS e tals por eventos é porque a gente não sabe programar, e aprender não é fácil  :o:


Se você faz marketing de uma engine como algo fácil que qualquer um sem conhecimento de programação pode fazer o jogo, mas pra fazer algo decente você precisa programar (ou usar scripts/plugins dos outros), você falhou como engine.

        

07/02/2021 às 13:12 #2 Última edição: 07/02/2021 às 13:30 por Ludo
Que viagem kkkkk
Melhor tutorial do planeta




Clique e conheça o meu canal - Canal RPG Maker Zone

07/02/2021 às 14:15 #3 Última edição: 07/02/2021 às 14:18 por Brandt
Citação de: Kerazzk online 07/02/2021 às 11:13
Amei o shitpost. Maaas

Mais um programador falando pra não fazermos coisas por eventos  :coffee:
Se a gente faz menus, ABS e tals por eventos é porque a gente não sabe programar, e aprender não é fácil  :o:


Se você faz marketing de uma engine como algo fácil que qualquer um sem conhecimento de programação pode fazer o jogo, mas pra fazer algo decente você precisa programar (ou usar scripts/plugins dos outros), você falhou como engine.

Bom, claro que é um programador falando isso, quem mais seria? hahaha

Eu acho bem falho esse argumento de "eu faço menu e ABS por evento porque não sei programar", porque os eventos são praticamente programação sem ter que saber sintaxe.

Se você for ver BASIC por exemplo, a semelhança com eventos é bem forte:

Código: BASIC
REM Main program follows
INPUT "What is your name: ", UserName$
PRINT "Hello "; UserName$
DO
   INPUT "How many stars do you want: ", NumStars
   CALL PrintSomeStars(NumStars)
   DO
      INPUT "Do you want more stars? ", Answer$
   LOOP UNTIL Answer$ <> ""
   Answer$ = LEFT$(Answer$, 1)
LOOP WHILE UCASE$(Answer$) = "Y"
PRINT "Goodbye "; UserName$
END


(REM é de "reminder", é um comentário; o resto é bem autoexplicativo)

Coloca uns "@>", ":" e umas quebras de linha aleatórias aí e você tem praticamente eventos.
A única "vantagem" é talvez não precisar decorar a sintaxe, e talvez não "ter que lidar" com conceitos como subrotinas e tipos de dados.

Concordo que a engine que tem como lema "faça jogos sem programar" podia ter umas ferramentas mais avançadas pra isso, mas em última instância isso não muda que tem coisas que você vai acabar passando do limite que a ferramenta oferece "sem ter que programar".
~ Masked

07/02/2021 às 16:23 #4 Última edição: 07/02/2021 às 16:25 por Syureri
É bem divertido trabalhar com nybbles, bits e bytes. Especialmente quando você está trabalhando num sistema embutido com pouca ou nenhuma RAM e precisa simular a funcionalidade dela criando um Stack Allocator, ou usando maneiras mais engenhosas como o que o pessoal fazia na era do gameboy ou gameboy advance (menos de 4mb de ram, meus amigos).

Algo interessante de se lembrar é que a maioria dos programadores vez ou outra se encontram em situações em que precisam se adaptar a não poder usar suas ferramentas de luxo. O conhecimento que você adquire com programação não é apenas sintaxe, e sim a lógica. Tem uma galera fazendo joguinhos em calculadora, emuladores de Gameboy no Minecraft usando redstones, e até mesmo reprodutores de vídeo dentro do Terraria. Alá:


Fico genuinamente impressionado com o que a galera faz com os mais primitivos operadores lógicos e espero um dia ser capaz de desenvolver algo do tipo ao menos como uma conquista pessoal.

Citação de: Brandt online 07/02/2021 às 14:15
Bom, claro que é um programador falando isso, quem mais seria? hahaha

Eu acho bem falho esse argumento de "eu faço menu e ABS por evento porque não sei programar", porque os eventos são praticamente programação sem ter que saber sintaxe.

Se você for ver BASIC por exemplo, a semelhança com eventos é bem forte:

Código: BASIC
REM Main program follows
INPUT "What is your name: ", UserName$
PRINT "Hello "; UserName$
DO
   INPUT "How many stars do you want: ", NumStars
   CALL PrintSomeStars(NumStars)
   DO
      INPUT "Do you want more stars? ", Answer$
   LOOP UNTIL Answer$ <> ""
   Answer$ = LEFT$(Answer$, 1)
LOOP WHILE UCASE$(Answer$) = "Y"
PRINT "Goodbye "; UserName$
END


(REM é de "reminder", é um comentário; o resto é bem autoexplicativo)

Coloca uns "@>", ":" e umas quebras de linha aleatórias aí e você tem praticamente eventos.
A única "vantagem" é talvez não precisar decorar a sintaxe, e talvez não "ter que lidar" com conceitos como subrotinas e tipos de dados.

Concordo que a engine que tem como lema "faça jogos sem programar" podia ter umas ferramentas mais avançadas pra isso, mas em última instância isso não muda que tem coisas que você vai acabar passando do limite que a ferramenta oferece "sem ter que programar".

Exatamente =D, eu não uso o rpg maker pra ficar me estressando com linha de código, se for pra isso eu pego outra engine mais apropriada e faço um jogo pra vender. Acredito que tem muita gente, assim como eu, que gosta de fazer sistemas por eventos como hobby, uma brincadeira, passa tempo, estudo de lógica, etc... é bem divertido XD.