Compare commits

...

No commits in common. "master" and "makefile" have entirely different histories.

19 changed files with 525 additions and 409 deletions

3
.gitignore vendored
View File

@ -1,4 +1,3 @@
__pycache__/
/__pycache__
/build-fx
/build-cg
/assets-fx/xcf

View File

@ -1,49 +0,0 @@
# Configure with [fxsdk build-fx] or [fxsdk build-cg], which provide the
# toolchain file and module path of the fxSDK
cmake_minimum_required(VERSION 3.18)
project(MystNB)
include(GenerateG1A)
include(GenerateG3A)
include(Fxconv)
find_package(Gint 2.1 REQUIRED)
set(SOURCES
src/animation.c
src/engine.c
src/game.c
src/main.c
)
# Shared assets, fx-9860G-only assets and fx-CG-50-only assets
set(ASSETS
# ...
)
set(ASSETS_fx
assets-fx/levels.png
assets-fx/lv1.png
assets-fx/tilesheet.png
assets-fx/title.png
assets-fx/spritesheet.png
assets-fx/font_mystere.png
assets-fx/map/lv1.txt
)
set(ASSETS_cg
assets-cg/example.png
# ...
)
fxconv_declare_assets(${ASSETS} ${ASSETS_fx} ${ASSETS_cg} WITH_METADATA)
fxconv_declare_converters(assets-fx/converters.py)
add_executable(myaddin ${SOURCES} ${ASSETS} ${ASSETS_${FXSDK_PLATFORM}})
target_compile_options(myaddin PRIVATE -Wall -Wextra -Os)
target_link_libraries(myaddin Gint::Gint)
if("${FXSDK_PLATFORM_LONG}" STREQUAL fx9860G)
generate_g1a(TARGET myaddin OUTPUT "MystNB.g1a"
NAME "MystNB" ICON assets-fx/icon.png)
elseif("${FXSDK_PLATFORM_LONG}" STREQUAL fxCG50)
generate_g3a(TARGET myaddin OUTPUT "MystNB.g3a"
NAME "MystNB" ICONS assets-cg/icon-uns.png assets-cg/icon-sel.png)
endif()

202
Makefile Normal file
View File

@ -0,0 +1,202 @@
#! /usr/bin/make -f
# Default Makefile for fxSDK add-ins. This file was probably copied there by
# the [fxsdk] program.
#---
#
# Configuration
#
include project.cfg
# Compiler flags
CFLAGSFX := $(CFLAGS) $(CFLAGS_FX) $(INCLUDE)
CFLAGSCG := $(CFLAGS) $(CFLAGS_CG) $(INCLUDE)
# Linker flags
LDFLAGSFX := $(LDFLAGS) $(LDFLAGS_FX)
LDFLAGSCG := $(LDFLAGS) $(LDFLAGS_CG)
# Dependency list generation flags
depflags = -MMD -MT $@ -MF $(@:.o=.d) -MP
# ELF to binary flags
BINFLAGS := -R .bss -R .gint_bss
# G1A and G3A generation flags
NAME_G1A ?= $(NAME)
NAME_G3A ?= $(NAME)
G1AF := -i "$(ICON_FX)" -n "$(NAME_G1A)" --internal="$(INTERNAL)"
G3AF := -n basic:"$(NAME_G3A)" -i uns:"$(ICON_CG_UNS)" -i sel:"$(ICON_CG_SEL)"
ifeq "$(TOOLCHAIN_FX)" ""
TOOLCHAIN_FX := sh3eb-elf
endif
ifeq "$(TOOLCHAIN_CG)" ""
TOOLCHAIN_CG := sh4eb-elf
endif
# fxconv flags
FXCONVFX := --fx --toolchain=$(TOOLCHAIN_FX)
FXCONVCG := --cg --toolchain=$(TOOLCHAIN_CG)
#
# File listings
#
NULL :=
TARGET := $(subst $(NULL) $(NULL),-,$(NAME))
ifeq "$(TARGET_FX)" ""
TARGET_FX := $(TARGET).g1a
endif
ifeq "$(TARGET_CG)" ""
TARGET_CG := $(TARGET).g3a
endif
ELF_FX := build-fx/$(shell basename -s .g1a $(TARGET_FX)).elf
BIN_FX := $(ELF_FX:.elf=.bin)
ELF_CG := build-cg/$(shell basename -s .g3a $(TARGET_CG)).elf
BIN_CG := $(ELF_CG:.elf=.bin)
# Source files
src := $(wildcard src/*.[csS] \
src/*/*.[csS] \
src/*/*/*.[csS] \
src/*/*/*/*.[csS])
assets-fx := $(wildcard assets-fx/*/*)
assets-cg := $(wildcard assets-cg/*/*)
# Object files
obj-fx := $(src:%=build-fx/%.o) \
$(assets-fx:assets-fx/%=build-fx/assets/%.o)
obj-cg := $(src:%=build-cg/%.o) \
$(assets-cg:assets-cg/%=build-cg/assets/%.o)
# Additional dependencies
deps-fx := $(ICON_FX)
deps-cg := $(ICON_CG_UNS) $(ICON_CG_SEL)
# All targets
all :=
ifneq "$(wildcard build-fx)" ""
all += all-fx
endif
ifneq "$(wildcard build-cg)" ""
all += all-cg
endif
#
# Build rules
#
all: $(all)
all-fx: $(TARGET_FX)
all-cg: $(TARGET_CG)
$(TARGET_FX): $(obj-fx) $(deps-fx)
@ mkdir -p $(dir $@)
$(TOOLCHAIN_FX)-gcc -o $(ELF_FX) $(obj-fx) $(CFLAGSFX) $(LDFLAGSFX)
$(TOOLCHAIN_FX)-objcopy -O binary $(BINFLAGS) $(ELF_FX) $(BIN_FX)
fxg1a $(BIN_FX) -o $@ $(G1AF)
$(TARGET_CG): $(obj-cg) $(deps-cg)
@ mkdir -p $(dir $@)
$(TOOLCHAIN_CG)-gcc -o $(ELF_CG) $(obj-cg) $(CFLAGSCG) $(LDFLAGSCG)
$(TOOLCHAIN_CG)-objcopy -O binary $(BINFLAGS) $(ELF_CG) $(BIN_CG)
mkg3a $(G3AF) $(BIN_CG) $@
# C sources
build-fx/%.c.o: %.c
@ mkdir -p $(dir $@)
$(TOOLCHAIN_FX)-gcc -c $< -o $@ $(CFLAGSFX) $(depflags)
build-cg/%.c.o: %.c
@ mkdir -p $(dir $@)
$(TOOLCHAIN_CG)-gcc -c $< -o $@ $(CFLAGSCG) $(depflags)
# Assembler sources
build-fx/%.s.o: %.s
@ mkdir -p $(dir $@)
$(TOOLCHAIN_FX)-gcc -c $< -o $@
build-cg/%.s.o: %.s
@ mkdir -p $(dir $@)
$(TOOLCHAIN_CG)-gcc -c $< -o $@
# Preprocessed assembler sources
build-fx/%.S.o: %.S
@ mkdir -p $(dir $@)
$(TOOLCHAIN_FX)-gcc -c $< -o $@ $(INCLUDE)
build-cg/%.S.o: %.S
@ mkdir -p $(dir $@)
$(TOOLCHAIN_CG)-gcc -c $< -o $@ $(INCLUDE)
# Images
build-fx/assets/img/%.o: assets-fx/img/%
@ mkdir -p $(dir $@)
fxconv --bopti-image $< -o $@ $(FXCONVFX) name:img_$(basename $*) $(IMG.$*)
build-cg/assets/img/%.o: assets-cg/img/%
@ mkdir -p $(dir $@)
fxconv --bopti-image $< -o $@ $(FXCONVCG) name:img_$(basename $*) $(IMG.$*)
# Fonts
build-fx/assets/fonts/%.o: assets-fx/fonts/%
@ mkdir -p $(dir $@)
fxconv -f $< -o $@ $(FXCONVFX) name:font_$(basename $*) $(FONT.$*)
build-cg/assets/fonts/%.o: assets-cg/fonts/%
@ mkdir -p $(dir $@)
fxconv -f $< -o $@ $(FXCONVCG) name:font_$(basename $*) $(FONT.$*)
# Binaries
build-fx/assets/bin/%.o: assets-fx/bin/%
@ mkdir -p $(dir $@)
fxconv -b $< -o $@ $(FXCONVFX) name:bin_$(basename $*) $(BIN.$*)
build-cg/assets/bin/%.o: assets-cg/bin/%
@ mkdir -p $(dir $@)
fxconv -b $< -o $@ $(FXCONVCG) name:bin_$(basename $*) $(BIN.$*)
# Custom conversions
build-fx/assets/%.o: assets-fx/%
@ mkdir -p $(dir $@)
fxconv --custom $< -o $@ $(FXCONVFX) type:$(subst /,,$(dir $*)) name:$(subst /,_,$(basename $*))
build-cg/assets/%.o: assets-cg/%
@ mkdir -p $(dir $@)
fxconv --custom $< -o $@ $(FXCONVCG) type:$(subst /,,$(dir $*)) name:$(subst /,_,$(basename $*))
#
# Cleaning and utilities
#
# Dependency information
-include $(shell find build* -name *.d 2> /dev/null)
build-fx/%.d: ;
build-cg/%.d: ;
.PRECIOUS: build-fx build-cg build-fx/%.d build-cg/%.d %/
clean-fx:
@ rm -rf build-fx/
clean-cg:
@ rm -rf build-cg/
distclean-fx: clean-fx
@ rm -f $(TARGET_FX)
distclean-cg: clean-cg
@ rm -f $(TARGET_CG)
clean: clean-fx clean-cg
distclean: distclean-fx distclean-cg
install-fx: $(TARGET_FX)
p7 send -f $<
install-cg: $(TARGET_CG)
@ while [[ ! -h /dev/Prizm1 ]]; do sleep 0.25; done
@ while ! mount /dev/Prizm1; do sleep 0.25; done
@ rm -f /mnt/prizm/$<
@ cp $< /mnt/prizm
@ umount /dev/Prizm1
@- eject /dev/Prizm1
.PHONY: all all-fx all-cg clean distclean install-fx install-cg

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,12 +0,0 @@
*.png:
type: bopti-image
name_regex: (.*)\.png img_\1
font_mystere.png:
type: font
name: font_mystere
charset: print
grid.size: 5x7
grid.padding: 1
proportional: true

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 167 B

After

Width:  |  Height:  |  Size: 167 B

View File

Before

Width:  |  Height:  |  Size: 580 B

After

Width:  |  Height:  |  Size: 580 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 860 B

After

Width:  |  Height:  |  Size: 860 B

View File

Before

Width:  |  Height:  |  Size: 508 B

After

Width:  |  Height:  |  Size: 508 B

View File

@ -1,3 +0,0 @@
*.txt:
custom-type: map
name_regex: (.*)\.txt map_\1

View File

@ -3,11 +3,11 @@ import re
import os.path
def convert(input, output, params, target):
if params["custom-type"] == "map":
if params["type"] == "map":
convert_map(input, output, params, target)
return 0
else:
return 1
raise fxconv.FxconvError(f"unknown conversion {params['type']}")
sys.exit(1)
def convert_map(input, output, params, target):
TILE_AIR = 0

87
project.cfg Normal file
View File

@ -0,0 +1,87 @@
#---
# fxSDK project configuration file for MystNB
#---
# Project name, should be at most 8 bytes long.
# (You can also specify NAME_G1A or NAME_G3A to override individually.)
NAME := MystNB
# Internal name, should be '@' followed by at most 7 uppercase letters.
# WARNING: If this convention is not followed, the add-in might not appear in
# the main menu of the calculator!
INTERNAL := @MYSTNB
# Output file name. The default is to take <NAME>, replace spaces with dashes,
# and add .g1a (or .g3a). You can specify a different folder if you want.
TARGET_FX :=
TARGET_CG :=
# fx-9860G icon location
ICON_FX = assets-fx/icon-fx.png
# fx-CG 50 icon locations
ICON_CG_UNS = assets-cg/icon-cg-uns.png
ICON_CG_SEL = assets-cg/icon-cg-sel.png
#---
# Toolchain selection
#---
# Toolchain for fx9860g. Please see also CFLAGS_FX below.
TOOLCHAIN_FX := sh-elf
# Toolchain for fxcg50. Please see also CFLAGS_CG below.
TOOLCHAIN_CG := sh-elf
#---
# Compiler flags
#---
# Base compiler flags for the fxSDK, you usually want to keep these.
CFLAGS := -mb -ffreestanding -nostdlib -fstrict-volatile-bitfields
# Platform-specific compiler flags.
# <> If you are using sh3eb-elf, use -m3. (You can do this on both FX and CG.)
# <> If you are using sh4eb-elf, use -m4-nofpu. (Not ideal on FX but works.)
# <> If you are using sh4eb-nofpu-elf, then your compiler will likely use the
# FPU and cause problems on the calculator. Consider another configuration.
# <> If you are using an sh-elf with several targets, specify whichever you
# support. I recommend -m3 on FX and -m4-nofpu on CG.
# Please see also TOOLCHAIN_FX and TOOLCHAIN_CG above.
CFLAGS_FX := -D FX9860G -m3
CFLAGS_CG := -D FXCG50 -m4-nofpu
# Additional compiler flags, change to your own taste!
CFLAGS += -Wall -Wextra -Os
# Include paths. Add one -I option for each folder from which you want to
# be able to include files with #include<>.
INCLUDE := -I include
# Libraries. Add one -l option for each library you are using, and also
# suitable -L options if you have library files in custom folders. To use
# fxlib, add libfx.a to the project directory and use "-L . -lfx".
LIBS_FX :=
LIBS_CG :=
# Base linker flags for the fxSDK, you usually want to keep these.
LDFLAGS_FX := -T fx9860g.ld -lgint-fx $(LIBS_FX) -lgint-fx -lgcc
LDFLAGS_CG := -T fxcg50.ld -lgint-cg $(LIBS_CG) -lgint-cg -lgcc
# Additional linker flags, if you need any.
LDFLAGS :=
# Additional platform-specific linker flags.
LDFLAGS_FX += -Wl,-Map=build-fx/map -Wl,--print-memory-usage
LDFLAGS_CG += -Wl,-Map=build-cg/map -Wl,--print-memory-usage
#---
# File conversion parameters
#---
# Here you can add fxconv options for each converted file, individually.
# The syntax is "<type>.<file>". For example, to specify the parameters for a
# font named "hexa.png", you might write:
#
# FONT.hexa.png = charset:print grid.size:3x5 grid.padding:1
FONT.mystere.png = charset:print grid.size:5x7 grid.padding:1 proportional:true

View File

@ -64,8 +64,7 @@ bool play_level(int level)
/* Start the frame timer */
static volatile int tick = 1;
int t = timer_configure(TIMER_ANY, ENGINE_TICK*1000,
GINT_CALL(callback_tick, &tick));
int t = timer_setup(TIMER_ANY, ENGINE_TICK*1000, callback_tick, &tick);
if(t < 0) return false;
timer_start(t);

View File

@ -12,46 +12,43 @@ Avant de commencer, notez que tout ce tutoriel est suivi sur [url=https://gitea.
[brown][b]Création du projet[/b][/brown]
Commençons par créer les fichiers dont on a besoin. Le fxSDK fournit un modèle de projet avec tout ce qu'il faut pour compiler, via l'outil [b][brown]`fxsdk`[/brown][/b]. Le projet sera compilé avec CMake ; vous n'êtes pas obligés de comprendre les fins détails mais je vous invite à lire le [url=https://www.planet-casio.com/Fr/forums/topic16647-1-tutoriel-compiler-des-add-ins-avec-cmake-fxsdk.html]tutoriel de compilation d'add-ins avec CMake[/url] qui explore cet aspect en détail. Pour l'instant, mettez-vous dans un dossier de votre choix (pour moi `~/Programs`) et créez un nouveau projet :
Commençons par créer les fichiers dont on a besoin. Le fxSDK fournit un modèle de projet avec tout ce qu'il faut pour compiler, via l'outil [b][brown]`fxsdk`[/brown][/b]. Cependant, son ambition n'est que de fournir une base, pas d'être exhaustif. Si votre projet devient compliqué, il est attendu que vous compreniez comment la compilation fonctionne pour prendre en main votre application. On verra plus tard ce que ça veut dire concrètement ; pour l'instant, commençons avec le modèle. Mettez-vous dans un dossier de votre choix (pour moi `~/Programs`) et créez un nouveau projet :
[code]% fxsdk new mystere-noir-et-blanc
Created a new project Mystere- (build system: CMake).
Type 'fxsdk build-fx' or 'fxsdk build-cg' to compile the program.[/code]
Le fxSDK a créé l'add-in automatiquement. Voyons voir ce qu'il y a dans ce dossier pour commencer ! Vous pouvez le parcourir interactivement sur Gitea en consultant le commit [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/commit/fd94704d0ce1f656ea130b35ab16465f7cddd26d]`fd94704d0`[/url].
Creating a new project in folder 'mystere-noir-et-blanc'.
[i](Note : Ce tutoriel a été commencé en 2020, à une époque où le fxSDK produisait un projet avec un Makefile. J'ai ensuite réécrit l'historique en 2021 quand CMake a été ajouté. Si vous voulez la version Makefile du début de ce tutoriel, consultez la branche [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/branch/makefile]`makefile`[/url].)[/i]
Full project name ? (at most 8 characters)
> MystNB
Internal name ? ('@' followed by at most 7 uppercase letters)
(Add-in might not appear on calc if format is wrong)
> @MYSTNB
Your project 'MystNB' has been created.
Type 'fxsdk build-fx' or 'fxsdk build-cg' to compile the program.[/code]
Le fxSDK vous demande quelques informations sur votre add-in. Le premier est le nom de l'add-in tel qu'on le voit dans l'onglet `VERSION` de l'application `SYSTEM`, et le second est le nom interne qui sert notamment à attribuer des dossiers dans la mémoire principale. Mettez bien que chose de la forme `@NOM` sinon l'add-in ne s'affiche parfois pas dans le menu. ^^
Maintenant voyons voir ce qu'il y a dans ce dossier !
[code]% tree mystere-noir-et-blanc
mystere-noir-et-blanc
├── assets-cg
│   ├── example.png
│   ├── fxconv-metadata.txt
│   ├── icon-sel.png
│   └── icon-uns.png
│   ├── icon-cg-sel.png
│   └── icon-cg-uns.png
├── assets-fx
│   ├── example.png
│   ├── fxconv-metadata.txt
│   └── icon.png
├── CMakeLists.txt
│   └── icon-fx.png
├── Makefile
├── project.cfg
└── src
└── main.c[/code]
Le fxSDK a créé un certain nombre de dossiers et fichiers. Voici à quoi ils servent :
• `assets-cg` contient toutes les images, polices et autres ressources pour la Graph 90+E. Le fxSDK permet de programmer pour la Graph 90+E, et même de faire un add-in pour les Graph mono et la Graph 90+E en même temps. Ici, on va programmer uniquement sur Graph mono, donc je vais l'ignorer.
• `assets-cg` contient toutes les images, polices et autres ressources pour la Graph 90+E. Le fxSDK permet de programmer pour la Graph 90+E, et même de faire un add-in pour les Graph mono et la Graph 90+E en même temps. Ici, on va programmer uniquement sur Graph mono, donc je vais le supprimer.
• `assets-fx` contient les images, polices et autres ressources pour les Graph mono. C'est là qu'on va mettre la plupart de nos données ! Toutes les Graph mono sont identiques du point de vue de gint, de la Graph 75+E que j'utilise aux Graph 35+ USB, 35+E, 35+E II, et même les vieilles SH3, tout est compatible.
• Le `CMakeLists.txt` est un fichier indiquant comment compiler l'application. Quand vous tapez "`fxsdk build-fx`" dans le terminal, c'est lui qui donne toutes les instructions.
• Le `Makefile` est un fichier indiquant comment compiler l'application. Quand vous tapez "`make`" ou "`fxsdk build-fx`" dans le terminal, c'est lui qui donne toutes les instructions. Vous avez ici un Makefile fourni par le fxSDK qui suffit pour les add-ins simples. Comprendre comment il est écrit nécessiterait un tutoriel entier, donc pour l'instant on ne va pas le lire du tout.
• `project.cfg` est un fichier de configuration créé par le fxSDK pour personnaliser le comportement du Makefile. On s'en servira très vite pour ajouter la police d'écriture du jeu, et vous pouvez aussi y changer plein d'options de compilation. :)
• `src` contient comme d'habitude tous les fichiers de code. Le fxSDK a copié un `main.c` avec un code d'exemple.
Je ne vais pas beaucoup utiliser le CMakeLists.txt dans ce tutoriel, mais on va prendre un instant pour y indiquer des noms plus appropriés pour l'add-in et le fichier g1a que ce que le fxSDK a mis par défaut. Dans le fichier, cherchez l'appel à `generate_g1a()`, qui ressemble à ça :
[code] generate_g1a(TARGET myaddin OUTPUT "MyAddin.g1a"
NAME "MyAddin" ICON assets-fx/icon.png)[/code]
Le paramètre `NAME` est le nom de l'add-in dans le menu SYSTEM, et le paramètre `OUTPUT` est le nom du fichier g1a. On va remplacer `"MyAddin"` par `MystNB` :
[code] generate_g1a(TARGET myaddin OUTPUT "MystNB.g1a"
NAME "MystNB" ICON assets-fx/icon.png)[/code]
Et voilà, on peut attaquer le code. ^^
[brown][b]Hello, World![/b][/brown]
C'est parti ! Commençons avec le "Hello, World!" de gint que le fxSDK a copié dans `src/main.c` pour vous.
@ -84,7 +81,7 @@ On utilise pour éviter ça la fonction `getkey()`, qui met le programme en paus
La fonction `getkey()` est centrale, vous l'utiliserez pour toutes les entrées clavier sur tous les écrans qui ne sont pas en temps réel (comme les menus ou les applications utilitaires), et parfois même pour les écrans en temps réel dans les jeux. J'aimerais donc éclaircir trois choses au sujet de `getkey()` pour éviter toute confusion avec la fonction `GetKey()` que l'on trouve dans fxlib et que vous avez peut-être déjà utilisée.
1. `getkey()` [b]attend[/b]. Le code qui suit (le `return`) ne sera pas exécuté tant que l'utilisateur n'aura pas appuyé sur une touche, peu importe si ça lui prend 20 minutes !
1. `getkey()` [b]attend[/b]. Le code qui suit (le `return`) ne sera pas exécuté tant que l'utilisateur n'aura pas appuyé sur une touche, peu importe si ça lui prend des heures !
2. `getkey()` [b]renvoie[/b] un événement, alors que `GetKey()` modifie un pointeur qu'on lui passe en argument.
3. `getkey()` [b]ne rafraîchit pas l'écran[/b], contrairement à `GetKey()` qui appelle `Bdisp_PutDisp_DD()` avant de se mettre en attente. Il faut appeler `dupdate()` explicitement.
@ -92,68 +89,35 @@ Pour l'instant on ignore complètement la valeur de retour de `getkey()` (l'év
[brown][b]Compiler et tester[/b][/brown]
La compilation d'un add-in mérite des tutoriels complets ; vous pouvez découvrir les grands principes dans le [url=https://www.planet-casio.com/Fr/forums/topic15930-1-tdm-n016-grands-principes-de-compilation.html]Tutoriel du Mercredi #16[/url] et les spécificités de CMake dans le [url=https://www.planet-casio.com/Fr/forums/topic16647-1-tutoriel-compiler-des-add-ins-avec-cmake-fxsdk.html]tutoriel de compilation d'add-ins avec CMake[/url]. Pour l'instant, on va ignorer tous ces détails et utiliser le système que le fxSDK a copié pour nous. Pour compiler votre application pour la famille des Graph mono (Graph 35+E et affiliées), utilisez la commande "`fxsdk build-fx`". (Dans le fxSDK, "`fx`" représente la famille des Graph mono, tandis que "`cg`" représente les Prizm et Graph 90+E. Naturellement il existe aussi "`fxsdk build-cg`" pour compiler une version Graph 90+E.)
La compilation d'un add-in mérite un tutoriel complet, que j'écrirai sans doute un jour si personne ne me grille la politesse. Pour l'instant, on va ignorer tous ces détails et utiliser le Makefile que le fxSDK a copié pour nous. On se rappelle que ce Makefile vient avec un fichier `project.cfg` dans lequel vous pouvez modifier des options : on en aura besoin très vite. Pour compiler votre application pour la famille des Graph mono (Graph 35+E et affiliées), utilisez la commande "`fxsdk build-fx`". (Dans le fxSDK, "`fx`" représente la famille des Graph mono, tandis que "`cg`" représente les Prizm et Graph 90+E. Naturellement il existe aussi "`fxsdk build-cg`" pour compiler une version Graph 90+E.)
[code]% fxsdk build-fx
-- The C compiler identification is GNU 10.2.0
-- The CXX compiler identification is GNU 10.2.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /home/el/.local/bin/sh-elf-gcc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /home/el/.local/bin/sh-elf-g++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found Gint: TRUE (found suitable version "2.2.1-6", minimum required is "2.1")
-- Configuring done
-- Generating done
-- Build files have been written to: /tmp/mystere-noir-et-blanc/build-fx
Scanning dependencies of target myaddin
[ 33%] Building C object CMakeFiles/myaddin.dir/src/main.c.obj
[ 66%] Building FXCONV object CMakeFiles/myaddin.dir/assets-fx/example.png
[100%] Linking C executable myaddin
[100%] Built target myaddin[/code]
:: Making into build-fx
sh-elf-gcc -c src/main.c -o build-fx/src/main.c.o -mb -ffreestanding -nostdlib -fstrict-volatile-bitfields -Wall -Wextra -Os -D FX9860G -m3 -I include -MMD -MT build-fx/src/main.c.o -MF build-fx/src/main.c.d -MP
sh-elf-gcc -o build-fx/MystNB.elf build-fx/src/main.c.o -mb -ffreestanding -nostdlib -fstrict-volatile-bitfields -Wall -Wextra -Os -D FX9860G -m3 -I include -T fx9860g.ld -lgint-fx -lgint-fx -lgcc -Wl,-Map=build-fx/map
sh-elf-objcopy -O binary -R .bss -R .gint_bss build-fx/MystNB.elf build-fx/MystNB.bin
fxg1a build-fx/MystNB.bin -o MystNB.g1a -i "assets-fx/icon-fx.png" -n "MystNB" --internal="@MYSTNB"[/code]
Il y a pas mal de détails sordides ici, mais vous devez pouvoir comprendre une partie du texte qui apparaît à l'écran, car c'est ici que vous aurez quasiment tous vos messages d'erreur pendant le développement. Bien comprendre les erreurs et qui vous les a envoyées vous évitera beaucoup de frustration et de temps perdu.
La première étape du processus est la [i]configuration[/i], où CMake analyse le fichier CMakeLists.txt, vérifie que le compilateur marche, que les bibliothèques comme gint sont bien là, et produit un Makefile qui permet de compiler l'application. Toutes les lignes au début qui ont un `--` sont des messages affichés par CMake durant la configuration.
Chaque ligne est une commande que le Makefile a lancé et qui contribue à compiler votre add-in. Le premier mot de chaque commande est le nom d'un outil qui a travaillé pour vous, les autres mots sont des options.
La deuxième étape du processus est la [i]compilation[/i], où le Makefile produit par CMake est lancé. Ce Makefile appelle GCC et affiche notamment les messages avec un pourcentage devant. À chacune des trois lignes en vert, make a lancé une commande pour compiler un fichier (les deux "Building ...") ou produire l'add-in final (le "Linking ...").
Par défaut, les commandes ne sont pas affichées, et on n'a que les lignes en vert. Mais si vous tapez `fxsdk build-fx -B VERBOSE=1` elles seront affichées. `VERBOSE=1` demande au Makefile d'afficher les commandes, et `-B` force à recompiler même si les sources n'ont pas changé (normalement si rien n'a changé rien n'est recompilé). Si vous faites ça, vous aurez (notamment) les lignes suivantes :
[code]% fxsdk build-fx -B VERBOSE=1
(...)
[ 33%] Building C object CMakeFiles/myaddin.dir/src/main.c.obj
/home/el/.local/bin/sh-elf-gcc -DFX9860G -DTARGET_FX9860G -mb -ffreestanding -nostdlib -Wa,--dsp -Wall -Wextra -Os -fstrict-volatile-bitfields -o CMakeFiles/myaddin.dir/src/main.c.obj -c /tmp/mystere-noir-et-blanc/src/main.c
[ 66%] Building FXCONV object CMakeFiles/myaddin.dir/assets-fx/example.png
fxconv /tmp/mystere-noir-et-blanc/assets-fx/example.png -o CMakeFiles/myaddin.dir/assets-fx/example.png --toolchain=sh-elf --fx
[100%] Linking C executable myaddin
/usr/bin/cmake -E cmake_link_script CMakeFiles/myaddin.dir/link.txt --verbose=1
/home/el/.local/bin/sh-elf-gcc -nostdlib -T fx9860g.ld CMakeFiles/myaddin.dir/src/main.c.obj CMakeFiles/myaddin.dir/assets-fx/example.png -o myaddin -lgcc -lgcc /home/el/.local/share/giteapc/Lephenixnoir/sh-elf-gcc/lib/gcc/sh3eb-elf/10.2.0/libgint-fx.a -lgcc
/home/el/.local/bin/sh-elf-objcopy -O binary -R .bss -R .gint_bss myaddin myaddin.bin
fxg1a myaddin.bin -n MyAddin -i /tmp/mystere-noir-et-blanc/assets-fx/icon.png -o /tmp/mystere-noir-et-blanc/MyAddin.g1a
[100%] Built target myaddin
(...)[/code]
Chaque ligne (excepté les messages du Makefile en vert) est une commande que le Makefile a lancé et qui contribue à compiler votre add-in. Le premier mot de chaque commande est le nom d'un outil qui a travaillé pour vous, les autres mots sont des options.
La première commande fait appel à [brown][b]sh-elf-gcc[/b][/brown] : c'est le compilateur. Il compile `src/main.c` (qui est mentionné tout à la fin de la ligne). La seconde fait appel à [brown][b]fxconv[/b][/brown], qui convertit l'image `assets-fx/example.png`. Les autres commandes utilisent [brown][b]sh-elf-gcc[/b][/brown], [brown][b]sh-elf-objcopy[/b][/brown] et [brown][b]fxg1a[/b][/brown] pour rassembler tous les résultats et créer un fichier g1a.
Les deux premières commandes font appel à [brown][b]`sh-elf-gcc`[/b][/brown] : c'est le compilateur. C'est lui qui transforme chaque fichier de code C en binaire et ensuite réunit ces binaires tous ensemble. Les deux commandes suivantes font appel à [brown][b]`sh-elf-objcopy`[/b][/brown], un copain de GCC, et [brown][b]`fxg1a`[/b][/brown], un outil du fxSDK. Ces deux vont ensemble et s'occupent de générer un fichier g1a avec le résultat de la compilation.
C'est pas grave si tout cela vous échappe un peu, j'y reviendrai de temps en temps. Pour l'instant, vous avez deux nouvelles choses dans le dossier de votre projet :
• Un dossier `build-fx` qui contient tous les fichiers compilés. Vous pouvez le supprimer à tout moment, mais il permet de recompiler l'application plus vite en récupérant le code déjà compilé quand vous ne l'avez pas modifié. Par exemple, si vous retapez `fxsdk build-fx` il ne se passe rien car vous n'avez rien modifié depuis la dernière compilation. En général vous voulez donc le laisser tranquille. Toutefois il n'est pas apprécié sur un dépôt Git donc ajoutez-le dans votre `.gitignore`. ;)
• Le fichier `MystNB.g1a` qui est notre add-in compilé !
Et c'est terminé ! :D Il ne reste qu'à envoyer l'add-in sur votre calculatrice par votre méthode préférée. Sur ma Graph 35+E II je l'enverrai par USB avec `fxlink -sw MystNB.g1a`. Pour les autres Graph, un outil de choix sous Linux est [url=https://www.planet-casio.com/Fr/forums/topic14487-1--GNU-Linux--P7,-pour-des-transferts-a-repasser.html]l'utilitaire P7 de Cakeisalie5[/url] (que `fxsdk send-fx` appelle pour vous !). Vous pouvez aussi utiliser FA-124, mais si vous en arrivez là je vous plains. ^^"
Et c'est terminé ! :D Il ne reste qu'à envoyer l'add-in sur votre calculatrice par votre méthode préférée. Sur ma Graph 35+E II je l'enverrai par USB. Pour les autres Graph, un outil de choix sous Linux est [url=https://www.planet-casio.com/Fr/forums/topic14487-1--GNU-Linux--P7,-pour-des-transferts-a-repasser.html]l'utilitaire P7 de Cakeisalie5[/url] (que `fxsdk send-fx` appelle pour vous !). Vous pouvez aussi utiliser FA-124, mais si vous en arrivez là je vous plains. ^^"
Une fois transféré, l'add-in apparaît dans le menu et on peut observer le résultat attendu :
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-01-sample-add-in.png[/img]
[i]Image 2 : Yeah![/i][/center]
Sur ce, il est temps de passer aux choses sérieuses !
L'état actuel du tutoriel correspond au commit [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/commit/de1281715b300a92dc7bf5261d9f98e62f11bb55]`de12817`[/url] dans l'historique du dépôt. Sur ce, il est temps de passer aux choses sérieuses !
[brown][b]Les assets[/b][/brown]
@ -177,98 +141,155 @@ Voici les assets que l'on va utiliser : une icône pour l'add-in, l'image du tit
Les images ci-dessus sont agrandies, bien sûr pour le projet il vous faut les originaux. Vous pouvez les télécharger directement sur le dépôt :
[center][url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/commit/f8a880bc8a49a53b3f094d6836fe31f38929c911/assets-fx][b]» Dossier `assets-fx` sur le dépôt à ce stade «[/b][/url][/center]
[center][url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/commit/5955e0472d77e7fbb9602336e8ec84fa5e30cd6e/assets-fx][b]» Dossier `assets-fx` sur le dépôt à ce stade «[/b][/url][/center]
Comme vous pouvez le voir, la police qu'on utilise est vraiment une image, c'est juste une grille de caractères. Le fxSDK va la convertir en une police utilisable avec `dtext()` en utilisant un de ses outils, [brown][b]`fxconv`[/b][/brown].
Téléchargez ou copiez les trois images dans `assets-fx` (celles du dépôt, pas les versions agrandies visibles sur cette page !). Au passage, je vais supprimer le fichier d'exemple `example.png`. Vous devez obtenir ceci :
Téléchargez ou copiez les trois images dans `assets-fx` (celles du dépôt, pas les versions agrandies visibles sur cette page !). L'icône du projet doit remplacer "`assets-fx/icon.png`", l'image `title.png` va dans un nouveau dossier "`assets-fx/img`", et la police `mystere.png` va dans un nouveau dossier "`assets-fx/fonts`". Ces noms de dossiers sont fixes, ce sont les mêmes pour tous les projets fxSDK. Vous devez obtenir ceci :
[code]% tree assets-fx
assets-fx
├── fxconv-metadata.txt
├── font_mystere.png
├── icon.png
├── levels.png
└── title.png[/code]
Pour pouvoir convertir tout cela avec [brown][b]fxconv[/b][/brown], il va falloir fournir quelques informations. Le résultat de la conversion sera une variable dans le programme C, il faut donc lui donner un nom. Et on a aussi plein d'informations à fournir sur la police, notamment quels caractères sont dessinés et à quel endroit. Toutes ces [i]métadonnées[/i] sont à indiquer dans un fichier `fxconv-metadata.txt`, et le fxSDK en a créé un pour nous. Voyons ce qu'il contient.
├── fonts
│   └── mystere.png
├── icon-fx.png
└── img
   ├── levels.png
   └── title.png[/code]
On utilise des sous-dossiers car le Makefile du fxSDK considère tout ce qui est dans `assets-fx/img` comme une image et tout ce qui est dans `assets-fx/fonts` comme une police, ça vous évite de le dire vous-même. Si vous tapez "`fxsdk build-fx`" maintenant, vous verrez que [brown][b]`fxconv`[/b][/brown] va automatiquement convertir l'image et la police. Enfin... il va essayer, car ça ne va pas marcher !
[code]example.png:
type: bopti-image
name: img_example[/code]
`fxconv-metadata.txt` contient une liste de blocs (ici 1). Chaque bloc sert à donner des informations sur un ou plusieurs fichiers : on indique d'abord le nom du fichier, et ensuite les paramètres qui s'y appliquent (avec une indentation). Les deux paramètres essentiels sont `type`, qui indique quelle conversion on veut faire, et `name`, qui indique le nom de la variable résultante. Ici, on convertit `example.png` comme une image bopti et on produit une variable `img_example` en résultat. bopti est un module de gint chargé de l'affichage des images, c'est lui qui fournit la fonction `dimage()` dont on se servira dans un instant.
[code]% fxsdk build-fx
On va modifier ça et donner les paramètres pour nos images et pour la police `font_mystere.png`
:: Making into build-fx
[code]levels.png:
type: bopti-image
name: img_levels
fxconv -f assets-fx/fonts/mystere.png -o build-fx/assets/fonts/mystere.png.o --fx --toolchain=sh-elf name:font_mystere
error: size of grid unspecified or invalid
make: *** [Makefile:147: build-fx/assets/fonts/mystere.png.o] Error 1[/code]
Prenons une seconde pour comprendre ce que ces quelques lignes nous disent. Comme précédemment, chaque ligne est une commande. Ici, on a lancé [brown][b]`fxconv`[/b][/brown], et on voit qu'une des options c'est le nom de notre fichier de police `assets-fx/fonts/mystere.png`, donc on l'a lancé pour convertir la police. Il a alors affiché la ligne d'en-dessous qui dit [i]"error: size of grid unspecified or invalid"[/i].
title.png:
type: bopti-image
name: img_title
Comme une erreur s'est produite, le Makefile s'arrête puisqu'il est impossible de finir la compilation, ce qui se voit à la dernière ligne avec `make: ***` et puis `Error 1` à la fin. Cette ligne apparaît toujours quand il y a une erreur, mais elle ne dit pas quelle est l'erreur : c'est juste `make` qui dit [i]« au cas où vous auriez pas vu, un truc a planté, donc je m'arrête »[/i]. Ici l'erreur c'est bien celle qui parle de la taille de la grille.
font_mystere.png:
type: font
charset: print
grid.size: 5x7
grid.padding: 1
proportional: true[/code]
Pour les images, rien de très nouveau. Pour la police il y a plus d'informations ; voyons ce qu'elles veulent dire. :)
En fait, [brown][b]`fxconv`[/b][/brown] ne sait pas comment notre police est construite. On lui a donné une grosse image sans indiquer comment il doit se débrouiller pour y lire des caractères. Il faut au moins lui dire quels caractères on a dessinés (B ? é ? ∀ ?) et leurs dimensions. Et ça, on le fait en modifiant le fichier de configuration du projet, `project.cfg`. C'est un fichier texte que vous pouvez modifier comme un fichier C. On y trouve plein de paramètres, auxquels on va tout de suite ajouter les informations dont [brown][b]`fxconv`[/b][/brown] a besoin pour convertir notre police.
[code]#---
# fxSDK project configuration file for MystNB
#---
# Project name, should be at most 8 bytes long.
# (You can also specify NAME_G1A or NAME_G3A to override individually.)
NAME := MystNB
# Internal name, should be '@' followed by at most 7 uppercase letters.
# WARNING: If this convention is not followed, the add-in might not appear in
# the main menu of the calculator!
INTERNAL := @MYSTNB
# Output file name. The default is to take <NAME>, replace spaces with dashes,
# and add .g1a (or .g3a). You can specify a different folder if you want.
TARGET_FX :=
TARGET_CG :=
# fx-9860G icon location
ICON_FX = assets-fx/icon-fx.png
# fx-CG 50 icon locations
ICON_CG_UNS = assets-cg/icon-cg-uns.png
ICON_CG_SEL = assets-cg/icon-cg-sel.png
#---
# Toolchain selection
#---
# Toolchain for fx9860g. Please see also CFLAGS_FX below.
TOOLCHAIN_FX := sh-elf
# Toolchain for fxcg50. Please see also CFLAGS_CG below.
TOOLCHAIN_CG := sh-elf
#---
# Compiler flags
#---
# Base compiler flags for the fxSDK, you usually want to keep these.
CFLAGS := -mb -ffreestanding -nostdlib -fstrict-volatile-bitfields
# Platform-specific compiler flags.
# <> If you are using sh3eb-elf, use -m3. (You can do this on both FX and CG.)
# <> If you are using sh4eb-elf, use -m4-nofpu. (Not ideal on FX but works.)
# <> If you are using sh4eb-nofpu-elf, then your compiler will likely use the
# FPU and cause problems on the calculator. Consider another configuration.
# <> If you are using an sh-elf with several targets, specify whichever you
# support. I recommend -m3 on FX and -m4-nofpu on CG.
# Please see also TOOLCHAIN_FX and TOOLCHAIN_CG above.
CFLAGS_FX := -D FX9860G -m3
CFLAGS_CG := -D FXCG50 -m4-nofpu
# Additional compiler flags, change to your own taste!
CFLAGS += -Wall -Wextra -Os
# Include paths. Add one -I option for each folder from which you want to
# be able to include files with #include<>.
INCLUDE := -I include
# Libraries. Add one -l option for each library you are using, and also
# suitable -L options if you have library files in custom folders. To use
# fxlib, add libfx.a to the project directory and use "-L . -lfx".
LIBS_FX :=
LIBS_CG :=
# Base linker flags for the fxSDK, you usually want to keep these.
LDFLAGS_FX := -T fx9860g.ld -lgint-fx $(LIBS_FX) -lgint-fx -lgcc
LDFLAGS_CG := -T fxcg50.ld -lgint-cg $(LIBS_CG) -lgint-cg -lgcc
# Additional linker flags, if you need any.
LDFLAGS :=
# Additional platform-specific linker flags.
LDFLAGS_FX += -Wl,-Map=build-fx/map
LDFLAGS_CG += -Wl,-Map=build-cg/map
#---
# File conversion parameters
#---
# Here you can add fxconv options for each converted file, individually.
# The syntax is "<type>.<file>". For example, to specify the parameters for a
# font named "hexa.png", you might write:
#
# FONT.hexa.png = charset:print grid.size:3x5 grid.padding:1[/code]
Il y a tellement de choses obscures là-dedans qu'on pourrait se demander si le mystère en noir et blanc ne serait pas en fait ce fichier. Ce qu'on va faire c'est ignorer royalement tout sauf les quelques lignes à la fin dans "File conversion parameters".
Tout ce dont vous avez besoin de savoir sur ce fichier pour l'instant se résume à deux points.
• Toutes les lignes qui commencent par un `#` sont des commentaires et toutes les autres donnent des valeurs à des paramètres avec une syntaxe du genre "`PARAMÈTRE = valeur`".
• Pour chaque fichier (image, police, etc) à convertir, il y a un paramètre (optionnel) qui nous permet de donner des informations à [brown][b]`fxconv`[/b][/brown].
Lors de la conversion des images et polices, le Makefile que le fxSDK a créé par nous va chercher toute information à destination de [brown][b]`fxconv`[/b][/brown] dans ce fichier. On va donc en ajouter tout à la fin avec la ligne suivante :
[code]FONT.mystere.png = charset:print grid.size:5x7 grid.padding:1 proportional:true[/code]
Notre fichier s'appelle `mystere.png`, et c'est une police. Le nom du paramètre qui va avec est `FONT.mystere.png`. Pour l'image `title.png`, ce serait `IMG.title.png`. Le reste c'est les informations supplémentaires ; voyons ce qu'elles veulent dire. :)
• "`charset:print`" indique quels sont les caractères qui sont dessinés. "`print`" c'est l'ensemble des caractères affichables de l'ASCII, il y en a 95 qui commencent par l'espace et se terminent par le tilde. (Le carré en bas à droite sera ignoré.) Avec cette information, [brown][b]`fxconv`[/b][/brown] sait que le 35ème caractère est "B".
• "`grid.size:5x7`" indique quelle taille fait chaque caractère. Comme vous pouvez le voir, on donne la même taille pour tout le monde, donc il faut indiquer une taille assez grande. La plupart des caractères font moins de 5 pixels de large, mais `M`, `W` et quelques autres nous obligent à utiliser une grille de largeur au moins 5. Pour la hauteur, la plupart des caractères font 6 pixels mais il y a des caractères comme `g` ou la virgule qui descendent une ligne plus bas que les autres, portant le total à 7. Cette grille dit à [brown][b]fxconv[/b][/brown] où les caractères sont dessinés dans notre PNG, mais ce n'est pas forcément la taille finale à l'écran de la calculatrice.
• "`grid.size:5x7`" indique quelle taille fait chaque caractère. Comme vous pouvez le voir, on donne la même taille pour tout le monde, donc il faut indiquer une taille assez grande. La plupart des caractères font moins de 5 pixels de large, mais `M`, `W` et quelques autres nous obligent à utiliser une grille de largeur au moins 5. Pour la hauteur, la plupart des caractères font 6 pixels mais il y a des caractères comme `g` ou la virgule qui descendent une ligne plus bas que les autres, portant le total à 7. Cette grille dit à [brown][b]`fxconv`[/b][/brown] où les caractères sont dessinés dans notre PNG, mais ce n'est pas forcément la taille finale à l'écran de la calculatrice.
• "`grid.padding:1`" indique qu'autour de chaque caractère, j'ai laissé un cadre blanc de 1 pixel de large. Je l'ai fait pour conserver un espacement confortable pendant l'édition. Vous pouvez le voir sur l'image suivante où j'ai mis le padding en valeur en bleu et jaune :
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-01-font-padding.png[/img]
[i]Image 6 : Il y a un pixel de padding autour de chaque caractère.[/i][/center]
• "`proportional:true`" signifie qu'on veut une police à largeur variable : on veut que chaque caractère prenne juste la place qui est nécessaire pour le dessiner sur la calculatrice. Ainsi, même si `M` ou `W` prennent 5 pixels, `I` fera quand même 1 pixel et `A` 4 pixels. En pratique, après avoir isolé chaque caractère en utilisant la grille, [brown][b]fxconv[/b][/brown] va encore éliminer le blanc à gauche et à droite.
• "`proportional:true`" signifie qu'on veut une police à largeur variable : on veut que chaque caractère prenne juste la place qui est nécessaire pour le dessiner sur la calculatrice. Ainsi, même si `M` ou `W` prennent 5 pixels, `I` fera quand même 1 pixel et `A` 4 pixels. En pratique, après avoir isolé chaque caractère en utilisant la grille, [brown][b]`fxconv`[/b][/brown] va encore éliminer le blanc à gauche et à droite.
Vous n'avez pas besoin de comprendre tous les détails de comment les polices marchent pour l'instant. Si des choses vous échappent, vous aurez l'occasion d'y revenir plus tard. ;)
Avant de pouvoir tester, il faut qu'on pense à modifier la liste des fichiers du projet dans `CMakeLists.txt`. Ici, j'ai simplement ajouté des assets pour Graph mono, donc je modifie la variable `ASSETS_fx`. Si vous n'êtes pas à l'aise avec cete partie de CMake, je vous invite à lire le [url=https://www.planet-casio.com/Fr/forums/topic16647-1-tutoriel-compiler-des-add-ins-avec-cmake-fxsdk.html]tutoriel d'introduction à CMake[/url] qui est très détaillé.
Pour les images, il n'y a aucun paramètre obligatoire, [brown][b]`fxconv`[/b][/brown] se débrouille tout seul pour convertir et sur Graph mono il n'y a généralement aucune raison de vouloir changer le comportement par défaut. Vous n'aurez donc pas besoin de modifier les paramètres de `project.cfg` trop souvent.
[code]set(ASSETS_fx
assets-fx/levels.png
assets-fx/title.png
assets-fx/spritesheet.png
assets-fx/font_mystere.png
# ...
)[/code]
Vous pouvez maintenant recompiler avec "`fxsdk build-fx`" et observer que l'image et la police sont converties !
Vous pouvez maintenant recompiler avec "`fxsdk build-fx`" et observer que l'image et la police sont converties ! Si vous en avez l'habitude, vous pouvez aussi juste taper "`make`", ce qui va compiler automatiquement dans `build-fx` (s'il existe) puis dans `build-cg` (s'il existe).
[code]% fxsdk build-fx
-- Configuring done
-- Generating done
-- Build files have been written to: /home/el/Programs/mystere-noir-et-blanc/build-fx
Scanning dependencies of target myaddin
Consolidate compiler generated dependencies of target myaddin
[ 20%] Building FXCONV object CMakeFiles/myaddin.dir/assets-fx/levels.png
[ 40%] Building FXCONV object CMakeFiles/myaddin.dir/assets-fx/title.png
[ 60%] Building FXCONV object CMakeFiles/myaddin.dir/assets-fx/font_mystere.png
[ 80%] Linking C executable myaddin
[100%] Built target myaddin[/code]
Vous pouvez voir qu'il y a de nouveau des lignes avec `--`. C'est parce qu'on a modifié `CMakeLists.txt`, du coup CMake reconfigure et recompile au lieu de simplement recompiler. De nouveau, si vous ajoutez `VERBOSE=1`, vous verrez le détail des commandes (`-B` là aussi est nécessaire pour forcer à recompiler) :
[code]% fxsdk build-fx -B VERBOSE=1
(...)
[ 40%] Building FXCONV object CMakeFiles/myaddin.dir/assets-fx/levels.png
fxconv /home/el/Programs/mystere-noir-et-blanc/assets-fx/levels.png -o CMakeFiles/myaddin.dir/assets-fx/levels.png --toolchain=sh-elf --fx
[ 60%] Building FXCONV object CMakeFiles/myaddin.dir/assets-fx/title.png
fxconv /home/el/Programs/mystere-noir-et-blanc/assets-fx/title.png -o CMakeFiles/myaddin.dir/assets-fx/title.png --toolchain=sh-elf --fx
[ 80%] Building FXCONV object CMakeFiles/myaddin.dir/assets-fx/font_mystere.png
fxconv /home/el/Programs/mystere-noir-et-blanc/assets-fx/font_mystere.png -o CMakeFiles/myaddin.dir/assets-fx/font_mystere.png --toolchain=sh-elf --fx
[100%] Linking C executable myaddin
/usr/bin/cmake -E cmake_link_script CMakeFiles/myaddin.dir/link.txt --verbose=1
/home/el/.local/bin/sh-elf-gcc -nostdlib -T fx9860g.ld CMakeFiles/myaddin.dir/src/main.c.obj CMakeFiles/myaddin.dir/assets-fx/levels.png CMakeFiles/myaddin.dir/assets-fx/title.png CMakeFiles/myaddin.dir/assets-fx/font_mystere.png -o myaddin -lgcc /home/el/.local/share/giteapc/Lephenixnoir/sh-elf-gcc/lib/gcc/sh3eb-elf/10.2.0/libgint-fx.a -lopenlibm -lgcc
/home/el/.local/bin/sh-elf-objcopy -O binary -R .bss -R .gint_bss myaddin myaddin.bin
fxg1a -n MystNB -i /home/el/Programs/mystere-noir-et-blanc/assets-fx/icon.png -o /home/el/Programs/mystere-noir-et-blanc/MystNB.g1a myaddin.bin
[100%] Built target myaddin[/code]
Reprenons un instant pour voir ce qui s'est passé. [brown][b]`fxconv`[/b][/brown] a été appelé deux fois, une pour convertir la police et une pour convertir l'image. Ensuite [brown][b]`sh-elf-gcc`[/b][/brown] a été appelé pour fusionner le résultat de la conversion avec le résultat de la compilation de `main.c`. Enfin, [brown][b]`sh-elf-objcopy`[/b][/brown] et [brown][b]`fxg1a`[/b][/brown] ont été appelés de nouveau pour recréer le fichier g1a. C'est un peu difficile à lire parce que les commandes sont compliquées, mais voici quelques pistes :
• Les lignes qui commencent par `/usr/bin/cmake -E` peuvent être ignorées.
• Les noms de fichiers à rallonge comme `CMakeFiles/myaddin.dir/src/main.c.obj` n'ont pas vraiment d'importance : regardez seulement le dernier morceau, `main.c.obj`. C'est ce dont vous avez besoin pour comprendre de quel fichier il s'agit.
[code]% make
fxconv -f assets-fx/fonts/mystere.png -o build-fx/assets/fonts/mystere.png.o --fx --toolchain=sh-elf name:font_mystere charset:print grid.size:5x7 grid.padding:1 proportional:true
fxconv --bopti-image assets-fx/img/title.png -o build-fx/assets/img/title.png.o --fx --toolchain=sh-elf name:img_title
sh-elf-gcc -o build-fx/MystNB.elf build-fx/src/main.c.o build-fx/assets/fonts/mystere.png.o build-fx/assets/img/title.png.o -mb -ffreestanding -nostdlib -fstrict-volatile-bitfields -Wall -Wextra -Os -D FX9860G -m3 -I include -T fx9860g.ld -lgint-fx -lgint-fx -lgcc -Wl,-Map=build-fx/map
sh-elf-objcopy -O binary -R .bss -R .gint_bss build-fx/MystNB.elf build-fx/MystNB.bin
fxg1a build-fx/MystNB.bin -o MystNB.g1a -i "assets-fx/icon-fx.png" -n "MystNB" --internal="@MYSTNB"[/code]
Reprenons un instant pour voir ce qui s'est passé. [brown][b]`fxconv`[/b][/brown] a été appelé deux fois, une pour convertir la police et une pour convertir l'image. Ensuite [brown][b]`sh-elf-gcc`[/b][/brown] a été appelé pour fusionner le résultat de la conversion avec le résultat de la compilation de `main.c`, qui avait été produit lors de la première compilation. Enfin, [brown][b]`sh-elf-objcopy`[/b][/brown] et [brown][b]`fxg1a`[/b][/brown] ont été appelés de nouveau pour recréer le fichier g1a. Contrairement à tout à l'heure, le compilateur [brown][b]`sh-elf-gcc`[/b][/brown] n'a été appelé qu'une fois car il n'a pas été nécessaire de recompiler `main.c` : le code n'a pas changé depuis la dernière fois !
[brown][b]Dessiner un menu principal[/b][/brown]
@ -318,7 +339,14 @@ Ce sont des déclarations de variables. Les deux premières variables sont du ty
Les deux sont marquées `extern`, ce qui est très important : ça signifie qu'on ne crée pas de variable (on les [i]déclare[/i] mais on ne les [i]définit[/i] pas, dans le jargon) ; on indique seulement au compilateur que ces variables existent [i]ailleurs[/i] et on lui promet de les lui fournir quand il en aura besoin. Ces variables sont en fait créées par [brown][b]`fxconv`[/b][/brown] lorsqu'il convertit les images et polices. :)
Désormais, plus besoin de tableaux longs et moches du genre `const unsigned char title[256] = { }` à copier-coller partout dans votre code, il vous suffit d'enregistrer votre image ou police dans `assets-fx`, de la déclarer dans `fxconv-metadata.txt` et `CMakeLists.txt`, et vous pouvez tout de suite l'utiliser dans le code avec `extern`. Le fxSDK se charge de la convertir et de l'ajouter à votre add-in durant la compilation. On verra plus tard comment nommer automatiquement les images pour éviter d'avoir à modifier `fxconv-metadata.txt`. ;)
Désormais, plus besoin de tableaux longs et moches du genre `const unsigned char title[256] = { }` à copier-coller partout dans votre code, il vous suffit de créer votre image ou police au bon endroit dans `assets-fx` puis de la déclarer dans le code avec `extern`. Le fxSDK se charge de la convertir et de l'ajouter à votre add-in, sous le nom suivant :
• Pour une image, "`img_`" suivi du nom du fichier. Par exemple "`title.png`" devient la variable "`img_title`".
• Pour une police, "`font_`" suivi du nom du fichier. Par exemple "`island.png`" devient la variable "`font_island`".
Ce sont les noms par défaut mais vous pouvez les changer plus tard en ajoutant des paramètres à `project.cfg`. Notez que du coup vous devez utiliser des underscore (`_`) dans les noms de fichiers de `assets-fx` car les autres caractères (espace, tiret) sont interdits dans les noms de variables en C.
Voyons la suite.
[code]dfont(&font_mystere);[/code]
Ensuite, on change de police avec un appel à la fonction `dfont()`, qui prend en paramètre la nouvelle police à utiliser. Vous noterez qu'il faut donner [i]« l'adresse »[/i] de la variable (c'est ce petit "`&`" devant le nom). Si vous ne savez pas ce que c'est, pas grave. Retenez juste que dès que vous utiliserez une image ou une police de gint provenant de l'extérieur du programme il faudra systématiquement mettre ce "`&`" (libimg est la seule exception jusqu'ici).
@ -364,7 +392,7 @@ C'était un peu gros mais on en reverra. Une fois tout dessiné, on n'oublie pas
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-01-menu.png[/img]
[i]Image 7 : Le menu principal prend forme ![/i][/center]
Cet état d'avancement correspond au commit [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/commit/f8a880bc8a49a53b3f094d6836fe31f38929c911]`f8a880bc8`[/url] sur le dépôt du projet.
Cet état d'avancement correspond au commit [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/commit/5955e0472d77e7fbb9602336e8ec84fa5e30cd6e]`5955e04`[/url] sur le dépôt du projet.
[brown][b]Le curseur interactif[/b][/brown]
@ -446,7 +474,6 @@ Et voilà ! :bounce:
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-01-menu-animated.gif[/img]
[i]Image 8 : C'était pas si difficile ![/i][/center]
Le projet à ce stade peut être consulté au commit [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/commit/87c9bc3dfeff761f293dc047d83b73421c685b60]`87c9bc3df`[/url] sur le dépôt.
[brown][b]Conclusion[/b][/brown]
@ -455,7 +482,7 @@ C'est un menu très schématique et qu'on pourrait facilement améliorer... quel
Voilà un petit récapitulatif de ce qu'on a vu.
:here: Créer et compiler un projet avec le fxSDK
:here: Ajouter des assets et spécifier leurs paramètres pour [brown][b]fxconv[/b][/brown] dans `fxconv-metadata.txt`
:here: Ajouter des assets et spécifier leurs paramètres pour [brown][b]fxconv[/b][/brown] dans `project.cfg`
:here: Dessin général (`<gint/display.h>`) : `dclear()`, `drect()`, `dupdate()`
:here: Dessin d'images (`<gint/display.h>`) : `dimage()`, `dsubimage()`
:here: Dessin de texte (`<gint/display.h>`) : `dtext()`, `dprint()`, `dfont()`

View File

@ -18,16 +18,7 @@ On arrivera vite au point où le code et les fonctions de gint se complexifient
On va tout de suite commencer par définir les types fondamentaux du moteur. Je vais le faire dans deux nouveaux fichiers, `engine.h` et `engine.c`. Lorsqu'on programe avec plusieurs fichiers, il est important que chaque fichier source (`.c`) possède un en-tête associé (`.h`) qui récapitule les fonctions et types qui y sont définis. C'est parce que le compilateur compile tous les fichiers sources indépendamment les uns des autres, et c'est cette information qui lui permet de se faire une vision d'ensemble. Vous pouvez trouver de plus amples détails dans le [url=https://www.planet-casio.com/Fr/forums/topic15378-1-tdm-n07-ecrire-des-programmes-c-avec-plusieurs-fichiers.html]TDM n°7[/url] sur ce sujet.
En pratique, le fichier d'en-tête contient une description des types de données manipulés par le fichier source associé et les prototypes de ses fonctions publiques. C'est une sorte de « résumé » si vous voulez.
On n'oublie pas qu'il faut ajouter les fichiers source au `CMakeLists.txt` pour qu'ils soient compilés. Les fichiers d'en-tête sont détectés automatiquement donc on n'y touche pas.
[code]set(SOURCES
src/main.c
src/engine.c
# ...
)[/code]
Je vais commencer par ajouter dans `engine.h` la définition d'une structure `player` qui décrit l'information complète d'un joueur présent sur la map. J'en profite aussi pour choisir des valeurs pour désigner les quatre directions donc j'aurai souvent besoin.
En pratique, le fichier d'en-tête contient une description des types de données manipulés par le fichier source associé et les prototypes de ses fonctions publiques. C'est une sorte de « résumé » si vous voulez. Ici, je vais commencer par ajouter dans `engine.h` la définition d'une structure `player` qui décrit l'information complète d'un joueur présent sur la map. J'en profite aussi pour choisir des valeurs pour désigner les quatre directions donc j'aurai souvent besoin.
[code]#ifndef _MYSTNB_ENGINE_H
#define _MYSTNB_ENGINE_H
@ -63,51 +54,10 @@ Quant à la définition de la structure à proprement parler, rien de bien impre
Si vous n'êtes pas encore tout à fait familier·ère avec les structures, c'est le bon moment de se rappeler que cette définition ne crée aucune variable, elle explique simplement ce que les variables de type `struct player` contiendront quand on créera.
On a également besoin de la spritesheet pour pouvoir faire notre affichage. Voici à quoi elle ressemble ; comme d'habitude, ne récupérez pas cette image (qui est agrandie), prenez plutôt [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/commit/8dba2daeb3c9321d8eb284df0b9076e18addf0c4/assets-fx/spritesheet.png]la version originale sur le dépôt[/url]. Enregistrez-la sous le nom `assets-fx/spritesheet.png`.
On a également besoin de la spritesheet pour pouvoir faire notre affichage. Voici à quoi elle ressemble ; comme d'habitude, ne récupérez pas cette image (qui est agrandie), prenez plutôt [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/commit/8fa119937d7efee57b7b8d6a730266e72795f3f9/assets-fx/img/spritesheet.png]la version originale sur le dépôt[/url]. Enregistrez-la sous le nom `assets-fx/img/spritesheet.png`.
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-02-spritesheet.png[/img]
[i]Image 2 : Pour les diagonales, on repassera.[/i][/center]
Je profite de l'occasion pour améliorer le `fxconv-metadata.txt`. Pour l'instant, on a listé tous les fichiers à la main dedans, mais ça devient vite répétitif, surtout qu'il n'y a pas de paramètres particuliers. Nos images ressemblent à ça :
[code]levels.png:
type: bopti-image
name: img_levels
title.png:
type: bopti-image
name: img_title
spritesheet.png:
type: bopti-image
name: img_spritesheet[/code]
Ce serait bien de pouvoir créer un bloc qui traite toutes les images d'un coup et ensuite on n'en parle plus. Et ça tombe bien, car ça existe : on peut spécifier un wildcard dans le nom, par exemple `*.png`.
[code]*.png:
type: bopti-image
name: ...[/code]
Par contre on a un problème : on ne peut plus spécifier le nom puisqu'on ne connaît pas le nom du fichier (il est caché par l'étoile). Pour rattraper la situation, [brown][b]fxconv[/b][/brown] propose un paramètre `name_regex` qui permet d'analyser le nom du fichier et de calculer le nom de la variable avec le résultat de l'analyse.
[code]*.png:
type: bopti-image
name_regex: (.*)\.png img_\1[/code]
Si vous ne connaissez pas les expressions regulières (regex), ne vous inquiétez pas de la magie en jeu ici. Essentiellement, `(.*)\.png` signifie « le nom du fichier ressemble à `xxxx.png`, trouve-moi `xxxx` ». Et ensuite `img_\1` signifie « maintenant nomme-moi la variable `img_xxxx` ». Il vous suffit de le copier-coller aujourd'hui, vous n'aurez jamais besoin d'y retourner si ça ne vous intéresse pas. ^^
Avec ce code, toutes les images `.png` dans le dossier seront converties automatiquement. Il y a juste deux détails importants pour que ça ne gêne pas la conversion de la police :
• Les blocs se lus de haut en bas et les informations sont combinées. Par exemple pour `font_mystere.png`, le `type` et le `name_regex` [i]vont s'appliquer[/i], par contre le bloc `font_mystere.png` va s'appliquer aussi et remplacer le `type` et ajouter un `name`.
• Quand `name_regex` et `name` sont spécifiés tous les deux, `name` a la priorité.
Et on n'oublie pas d'ajouter les fichiers à `CMakeLists.txt` dans tous les cas. ^^
[code]set(ASSETS_fx
assets-fx/levels.png
assets-fx/title.png
assets-fx/spritesheet.png
assets-fx/font_mystere.png
# ...
)[/code]
Revenons à la conception du jeu.
Le joueur se situe bien sûr à l'intérieur d'un niveau que j'appelle [i]map[/i] par pure commodité. C'est simplement une grille avec des espaces libres, des murs, des portes, et quelques items importants placés au sol. Pour l'instant on ne vas pas trop se demander comment les représenter et les convertir (ce sera dans le tutoriel suivant), on va juste donner une version vague de la structure avec les dimensions de la map histoire de pouvoir déplacer le joueur sans sortir des bords.
[code]/* struct map: A map with moving doors and collectibles */
@ -319,7 +269,7 @@ Le jeu est au tour par tour et les portes changent d'état entre les tours (acti
Vous noterez que `level_finished` n'est jamais mis à 1 donc la boucle ne se termine jamais ; ce n'est pas grave parce qu'on peut toujours fuir vers le menu principal durant les appels à `getkey_opt()` en appuyant sur MENU. ^^
Et voilà le résultat ! Le code à cette étape est celui du commit [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/commit/8dba2daeb3c9321d8eb284df0b9076e18addf0c4]`8dba2daeb`[/url] dans l'historique du dépôt.
Et voilà le résultat ! Le code à cet étape est celui du commit [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/commit/8fa119937d7efee57b7b8d6a730266e72795f3f9]`8fa1199`[/url] dans l'historique du dépôt.
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-02-basic-movement.gif[/img]
[i]Image 6 : ... ouais en fait non, c'est tout nul.[/i][/center]
@ -348,7 +298,7 @@ Voyons voir comment tout cela marche en pratique. D'abord il nous faut l'en-têt
[code]#include <gint/timer.h>
#include <gint/clock.h>[/code]
La première étape de ce nouveau système consiste à choisir et paramétrer un timer. Cela se fait avec la fonction `timer_configure()` qui prend 3 paramètres : le timer à utiliser, le délai d'attente en microsecondes (oui c'est précis !), et une fonction à appeler lorsque le délai sera écoulé.
La première étape de ce nouveau système consiste à choisir et paramétrer un timer. Cela se fait avec la fonction `timer_setup()` qui prend 4 paramètres : le timer à utiliser, le délai d'attente en microsecondes (oui c'est précis !), une fonction à appeler lorsque le délai sera écoulé, et un argument pour la fonction (optionnel).
Prenons tout ça dans l'ordre. Le choix du timer n'est pas totalement évident, car il y a deux genres de timers sur la calculatrice :
@ -359,7 +309,7 @@ Chaque timer est identifié par un entier entre 0 et 8. Il faut qu'on en choisis
Ensuite, le délai en microsecondes. Ça c'est facile, c'est `ENGINE_TICK*1000` soit 25000.
Dernier argument : une fonction à appeler lorsque le délai arrive à expiration. Cette fonction est souvent appelée [i]callback[/i], et va nous servir à noter dans une variable qu'il faut générer un nouveau frame. Dans gint, elle décide aussi si le timer doit continue de compter (et rappeler le callback une fois le délai écoulé de nouveau) ou s'arrêter ; dans notre cas, on le laissera tourner à l'infini et on l'arrêtera seulement à la sortie de la boucle principale lorsque la partie sera terminée.
Dernier argument obligatoire : une fonction à appeler lorsque le délai arrive à expiration. Cette fonction est souvent appelée [i]callback[/i], et va nous servir à noter dans une variable qu'il faut générer un nouveau frame. Dans gint, elle décide aussi si le timer doit continue de compter (et rappeler le callback une fois le délai écoulé de nouveau) ou s'arrêter ; dans notre cas, on le laissera tourner à l'infini et on l'arrêtera seulement à la sortie de la boucle principale lorsque la partie sera terminée.
Voici à quoi ressemble le callback. Je prends un argument de type `volatile int *`, c'est un pointeur sur un entier. Utiliser ce pointeur permet de modifier un entier ailleurs dans le programme ; dans notre cas ce sera une variable de la fonction `main()`. Je reviendrai sur le `volatile` un peu plus tard. Dans la fonction, j'affecte la variable à 1 à travers le pointeur, puis je renvoie la valeur `TIMER_CONTINUE` qui dit à gint de laisser le timer continuer à compter. L'autre option serait de renvoyer `TIMER_STOP` pour arrêter le timer immédiatement.
@ -368,26 +318,24 @@ Voici à quoi ressemble le callback. Je prends un argument de type `volatile int
*tick = 1;
return TIMER_CONTINUE;
}[/code]
Pour faire simple, ce callback affecte une variable à 1 et relance le timer. La variable concernée est celle dont on donnera un pointeur en argument quand on va former le callback. Voici l'appel complet.
Pour faire simple, ce callback affecte une variable à 1 et relance le timer. La variable concernée est celle dont on donnera un pointeur en argument en utilisant le paramètre optionnel de `timer_setup()`. Voici l'appel complet.
[code]/* Global tick clock */
static volatile int tick = 1;
int t = timer_configure(TIMER_ANY, ENGINE_TICK*1000, GINT_CALL(callback_tick, &tick));[/code]
La macro `GINT_CALL()` permet de créer un « appel indirect » : on indique quelle fonction on voudra appeler et avec quels paramètres, mais on ne l'appelle pas toute de suite. C'est quelque chose que le langage C n'a pas vraiment prévu et donc il y a des limitations (maximum 4 arguments et pas n'importe quel types), mais ce n'est pas la peine de s'y attarder pour l'instant.
Si cette histoire de callback vous semble un peu compliquée, vous pouvez lire cette à appel à `timer_configure()` de la façon suivante : [i]« en utilisant un timer quelconque (`TIMER_ANY`), toutes les `ENGINE_TICK*1000` microsecondes, gint va appeler `callback_tick(&tick)` »[/i].
int t = timer_setup(TIMER_ANY, ENGINE_TICK*1000, callback_tick, &tick);[/code]
Si cette histoire de callback et d'argument vous semble un peu compliquée, vous pouvez lire cette à appel à `timer_setup()` de la façon suivant : [i]« en utilisant un timer quelconque (`TIMER_ANY`), toutes les `ENGINE_TICK*1000` microsecondes, gint va appeler `callback_tick(&tick)` »[/i].
Si on récapitule tout, cela veut dire que toutes les 25 millisecondes notre entier `tick` passe à 1. Et donc, si on met la valeur à 0 après avoir généré un frame, on peut dormir en attendant que la variable repasse à 1 et ça nous donnera un frame toutes les 25 ms ! :)
Le `volatile` est important dans cette histoire à cause des optimisations du compilateur. Le compilateur ne sait pas trop ce qu'est un timer et ne vas pas se douter que votre `sleep()` va se faire interrompre régulièrement pour changer la valeur de la variable `tick` (ce qui est bien normal). Lui il voit surtout qu'on affecte `tick` à 0 mais jamais à 1, donc il risque de transformer votre attente en boucle infinie. En ajoutant `volatile`, on affirme que la valeur change même si c'est invisible pour lui, ce qui évite l'optimisation et donc un bug difficile à détecter.
Avec tout ça, on a presque fini. `timer_configure()` nous renvoie le numéro du timer que gint a choisi pour nous (vous savez, celui qu'on avait la flemme de choisir nous-mêmes), ou un nombre négatif si par malheur il n'y a aucun timer disponible qui corresponde à nos besoins. En supposant que tout se passe bien, on peut démarrer notre timer avec `timer_start()`. gint ne le fait pas tout seul car il y a des cas (comme le moteur de gris) où on veut réserver et paramétrer un timer sans le démarrer tout de suite.
Avec tout ça, on a presque fini. `timer_setup()` nous renvoie le numéro du timer que gint a choisi pour nous (vous savez, celui qu'on avait la flemme de choisir nous-mêmes), ou un nombre négatif si par malheur il n'y a aucun timer disponible qui corresponde à nos besoins. En supposant que tout se passe bien, on peut démarrer notre timer avec `timer_start()`. gint ne le fait pas tout seul car il y a des cas (comme le moteur de gris) où on veut réserver et paramétrer un timer sans le démarrer tout de suite.
[code]if(t >= 0) timer_start(t);[/code]
Toutes les fonctions manipulant des timers, à part `timer_configure()`, prennent en premier argument le numéro du timer concerné. Notez que je protège `timer_start()` au cas où gint n'ait trouvé aucun timer, mais c'est purement par principe : d'une part on sait qu'il y en a forcément un disponible (gint n'en utilise que 2 sur 9), d'autre part si on n'obtient pas de timer le jeu va freezer très rapidement puisqu'il n'y aura rien pour réveiller le programme une fois qu'on se sera endormis. Je protège juste l'appel pour éviter un crash dans cette situation hypothétique.
Toutes les fonctions manipulant des timers, à part `timer_setup()`, prennent en premier argument le numéro du timer concerné. Notez que je protège `timer_start()` au cas où gint n'ait trouvé aucun timer, mais c'est purement par principe : d'une part on sait qu'il y en a forcément un disponible (gint n'en utilise que 2 sur 9), d'autre part si on n'obtient pas de timer le jeu va freezer très rapidement puisqu'il n'y aura rien pour réveiller le programme une fois qu'on se sera endormis. Je protège juste l'appel pour éviter un crash dans cette situation hypothétique.
Ensuite on aura la boucle principale, et après ça on pourra arrêter le timer avant de quitter la fonction `main()`. Pensez bien à libérer vos timers car c'est cette action qui les rend de nouveau disponibles du point de vue de `timer_configure()`. (Pour arrêter un timer sans le libérer, utilisez `timer_pause()`.)
Ensuite on aura la boucle principale, et après ça on pourra arrêter le timer avant de quitter la fonction `main()`. Pensez bien à libérer vos timers car c'est cette action qui les rend de nouveau disponibles du point de vue de `timer_setup()`. (Pour arrêter un timer sans le libérer, utilisez `timer_pause()`.)
[code]if(t >= 0) timer_stop(t);
return 1;[/code]
@ -505,7 +453,7 @@ Le travail de `engine_tick()` consiste simplement à réduire `duration` du temp
}[/code]
La fonction d'animation en elle-même s'occupe principalement de passer au sprite suivant et de recharger `duration`, et renvoie un entier pour indiquer si le joueur est « occupé » : c'est utilisé pour refuser le mouvement durant une animation de marche. L'animation de marche change aussi les valeurs de `dx` et `dy` pour déplacer visuellement le joueur au cours du temps, et effectue une transition vers l'animation par défaut lorsque le joueur arrive à sa destination.
Le code de cette nouvelle version du programme se trouve au commit [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/commit/30ab7bae0a6746365c86c1ca81b9aa1ea98a4775]`30ab7bae0`[/url] sur le dépôt. Voyez notamment [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/commit/30ab7bae0a6746365c86c1ca81b9aa1ea98a4775/src/animation.c]`animation.c`[/url] si le code détaillé des animations vous intéresse.
Le code de cette nouvelle version du programme se trouve au commit [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/commit/b8ae9615b84bd5b50c10138e93af543cf3187a50]`b8ae961`[/url] sur le dépôt. Voyez notamment [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/commit/b8ae9615b84bd5b50c10138e93af543cf3187a50/src/animation.c]`animation.c`[/url] si le code détaillé des animations vous intéresse.
Il est temps de regarder ce que tout ça nous donne sur la calculatrice... !

View File

@ -1,47 +1,43 @@
[center][big][brown][b]03 : Des portes... [i]beaucoup[/i] de portes.[/b][/brown][/big][/center]
Dans ce tutoriel, on va passer le jeu au niveau supérieur. Jusqu'ici, on a codé un menu principal et un petit moteur de déplacement avec des animations. Pour l'instant, on a laissé la map de côté et on a hardcodé les positions des murs pour éviter que le joueur ne sorte de l'écran. Tout cela change aujourd'hui, parce qu'on va ajouter une map animée et les premières mécaniques de jeu ! Plus précisément :
Dans ce tutoriel, on va passer le jeu au niveau supérieur. Jusqu'ici, on a codé un menu principal et un petit moteur de déplacement avec des animations. Pour l'instant, on a laissé la map de côté et on a hardcodé les positions des murs pour éviter que le joueur ne sorte de la map. Tout cela change aujourd'hui, parce qu'on va ajouter une map animée et les premières mécaniques de jeu ! Plus précisément :
• On va voir comment convertir des objets personnalisés (ici une map) avec [brown][b]fxconv[/b][/brown] ;
• On va étendre le moteur de jeu pour intégrer des éléments dynamiques sur la map ;
• Et je vais coder suffisamment de mécaniques pour faire marcher le premier niveau.
• On va voir comment convertir des objets personnalisés (ici une map) avec [brown][b]`fxconv`[/b][/brown] ;
• Et on va étendre le moteur de jeu pour intégrer des changements sur la map.
Le final ressemblera à ça !
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-03-level-1.gif[/img]
TODO
[i]Image 1 : Si je vous spoile la solution y'a plus vraiment de mystère ![/i][/center]
Il y a un peu moins de gint ici, et un peu plus de code du jeu. Si vous programmez votre propre jeu en lisant ce tutoriel, faites bien attention à la structure du code. Comme j'en ai parlé dans le tutoriel précédent, il est [i]très important[/i] de séparer le rendu, la simulation du jeu et la gestion des entrées avant que le programme ne devienne monstrueux et impossible à maintenir (... ce qui arrivera [i]tout seul[/i] sauf si vous y portez une attention particulière ^^).
Il y a un peu moins de gint ici, et un peu plus de code du jeu. Si vous programmez votre propre jeu en lisant ce tutoriel, faites bien attention à la structure du code. Comme j'en ai parlé dans le tutoriel précédent, il est [i]très important[/i] de séparer le rendu, la simulation du jeu et la gestion des entrées avant que le programme ne devienne monstrueux et impossible à maintenir (... parce que c'est ce qui arrivera si vous ne faites pas attention ^^).
[brown][b]Format de la map en tant qu'asset[/b][/brown]
Pour l'instant, c'est parti ! La map du premier niveau ressemblera à ça. Ça n'inclut pas toutes les mécaniques, mais les grandes idées y sont. On a la zone de départ en bas à gauche, la zone d'arrivée en bas à droite, et le but c'est de passer de l'un à l'autre en utilisant l'ouverture et la fermeture automatique des portes, dont il existe deux types : les horizontales et les verticales.
Pour l'instant, c'est parti ! La map du premier niveau ressemblera à ça. Ça n'inclut pas toutes les mécaniques, mais les grandes idées y sont. On a la zone de départ en bas à gauche, la zone d'arrivée en bas à droite, et le but c'est de passer de l'un à l'autre en utilisant l'ouverture et la fermeture automatique des portes, dont il existe deux types : horizontaux et verticaux.
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-03-map1.png[/img]
[i]Image 2 : Le titre a pas menti, y'a [b]beaucoup[/b] de portes.[/i][/center]
Je pourrais tenter de générer la map directement à partir de l'image, mais ça serait assez compliqué. Pour ce projet tout simple qui n'a que 8 maps toutes de la même taille, je vais simplement fournir une image « squelette » du niveau (juste les murs en gros) et le programme affichera les points de départ et d'arrivée, les portes, et autres items par-dessus. Le rôle et la position des objets seront décrits dans un fichier texte qu'on va convertir avec fxconv parce que ce serait quand même dommage d'avoir à décrire une map en C.
[i]Image 1 : Le titre a pas menti, y'a [b]beaucoup[/b] de portes.[/i][/center]
Je pourrais tenter de générer la map directement à partir de l'image, mais ça serait assez compliqué. Pour ce projet tout simple qui n'a que 8 maps toutes de la même taille, je vais simplement fournir une image « squelette » du niveau (juste les murs en gros) et le programme affichera les points de départ et d'arrivée, les portes, et autres items par-dessus. Le rôle et la position des objets seront décrits dans un fichier texte qu'on va convertir avec [brown][b]`fxconv`[/b][/brown] parce que ce serait quand même dommage d'avoir à décrire une map en C.
Un vrai projet super ambitieux éditerait certainement les maps dans un outil spécialisé comme [url=https://www.mapeditor.org/]Tiled[/url], auquel cas le fichier texte serait remplacé par le fichier de sauvegarde de Tiled. Le principe serait le même, je vais simplement me contenter du fichier texte par simplicité pour ce tutoriel. Il ressemblera à ça :
[code]##### #####
# ##### #
# b a #
# a a #
##A### ###A##
# a b #
# a a #
# ~ ##### @ #
##### #####
a: #.
b: .#
A: #.[/code]
Les `#` représentent des murs, les lettres minuscules représentent des portes verticales et les lettres minuscules des portes horizontales. `~` et `@` sont le point de départ et le point d'arrivée, respectivement.
À la fin, il y a des informations qui décrivent le cycle d'ouverture/fermeture de chaque porte (certaines portes ont le même cycle et donc utilisent la même lettre). Je ne voudrais pas vous gâcher les mécaniques donc pour l'instant je n'en dis pas plus. :p
À la fin, il y a des informations qui décrivent le cycle d'ouverture/fermeture de chaque porte (ici les portes sont synchronisées donc j'ai utilisé qu'une seule lettre). Je ne voudrais pas vous gâcher les mécaniques donc pour l'instant je n'en dis pas plus. :p
[brown][b]Format de la map en tant qu'objet du programme[/b][/brown]
Pour obtenir ces informations dans le jeu, on va commencer par ajouter dans le code (plus précisément dans `engine.h`) la structure qu'on veut obtenir à la fin. La conversion de la map devra prendre en entrée le fichier texte ci-dessus et produire en sortie une structure de map, donc c'est important de bien définir le format tout de suite. J'avais donné un début dans le tutoriel précédent, qu'on peut maintenant compléter avec les détails en plus.
Pour obtenir ces informations dans le jeu, on va commencer par ajouter dans le code (plus précisément dans `engine.h`) la structure qu'on veut obtenir à la fin. La conversion de la map devra prendre en entrée le fichier texte ci-dessus et produire en sortie structure de map, donc c'est important de bien définir le format tout de suite. J'avais donné un début dans le tutoriel précédent, qu'on peut maintenant compléter avec les détails en plus.
[code]/* struct map: A map with moving doors, collectibles, and fog */
struct map
@ -80,29 +76,28 @@ Je numérote l'air, les murs, les points de départ et d'arrivée de 0 à 3. Les
Moralement `tiles` dans la `struct map` c'est un tableau de `enum map_tile`. Mais comme les énumérations sont des `int` par défaut ça voudrait dire que chaque case de la map occupe 4 octets en mémoire, et jeux avoir un type qui occupe juste 1 octet. Donc j'ai mis `uint8_t` à la place ; ça revient au même vu que tout ça sont des entiers. ;)
Donc voilà ce qu'on doit produire : une structure comprenant 156 octets de données plus un pointeur (de 4 octets) pointant vers l'image de fond, et un autre pointeur (de 4 octets) pointant vers `w*h` octets de données que l'on va générer en même temps que la structure. Voyons voir comment faire ça avec fxconv. ;)
Donc voilà ce qu'on doit produire : une structure comprenant 156 octets de données plus un pointeur de 4 octets pointan vers l'image de fond, et un autre pointeur de 4 octets pointant vers `w*h` octets de données. Voyons voir comment faire ça avec [brown][b]`fxconv`[/b][/brown]. ;)
[brown][b]Conversion de la map avec fxconv[/b][/brown]
[brown][b]Conversion de la map avec `fxconv`[/b][/brown]
On ne dirait pas de loin, mais fxconv n'est pas juste un outil en ligne de commande. En fait, il est programmable. On peut ajouter dans chaque projet des conversions personnalisées programmées en Python. ;)
On ne dirait pas de loin, mais [brown][b]`fxconv`[/b][/brown] n'est pas juste un outil en ligne de commande. En fait, il est programmable. On peut ajouter dans chaque projet des conversions personnalisées programmées en Python. ;)
D'abord, on va enregistrer la map dans un autre sous-dossier de `assets-fx` pour ne pas trop mélanger nos assets. Prenons par exemple `assets-fx/map/lv1.txt`. Pour les métadonnées, on va utiliser `custom-type` au lieu de `type` pour indiquer à fxconv qu'on utilise une conversion personnalisée. Comme j'ai mis un sous-dossier, je crée bien un nouveau `fxconv-metadata.txt` dans `assets-fx/map`.
D'abord, on va enregistrer la map dans un autre sous-dossier de `assets-fx` qui n'est pas déjà utilisé par [brown][b]`fxconv`[/b][/brown]. Pour moi ce sera `map`, le fichier est donc `assets-fx/map/map1.txt`. Si on tente de compiler tout de suite, le fxSDK se rend compte que c'est un objet personnalisé et appele [brown][b]`fxconv`[/b][/brown] en ce sens, ce qu'on voit au `--custom` sur la ligne de commande.
[code]*.txt:
custom-type: map
name_regex: (.*)\.txt map_\1[/code]
Le type est donc un type personnalisé "`map`" ; la regex est la même que dans le tutoriel précédent, mais avec l'extension `.txt` pour les noms de fichiers et le préfixe `map_` pour les noms de variables.
[code]fxconv --custom assets-fx/map/lv1.txt -o build-fx/assets/map/lv1.txt.o --fx --toolchain=sh-elf type:map name:map_lv1
error: --custom specified but no [converters] module in wd[/code]
Le procédé de conversion personnalisé est assez direct : [brown][b]`fxconv`[/b][/brown] cherche un module `converters.py` à la racine du projet, et appelle sa fonction `convert` avec le nom du fichier à convertir et quelques infos en plus. Pour l'instant ce module n'existe pas donc il est confus, mais on va y remédier tout de suite.
Maintenant, on peut coder le convertisseur. Je vais le mettre dans `assets-fx/converters.py` ; vous pouvez le mettre où vous voulez. Voilà à quoi doit ressembler le fichier :
Voilà à quoi doit ressembler le module dans `converters.py` :
[code]import fxconv
def convert(input, output, params, target):
if params["custom-type"] == "map":
if params["type"] == "map":
convert_map(input, output, params, target)
return 0
else:
return 1
raise fxconv.FxconvError(f"unknown conversion {params['type']}")
sys.exit(1)
def convert_map(input, output, params, target):
# Generate the data here...
@ -110,13 +105,13 @@ def convert_map(input, output, params, target):
fxconv.elf(data, output, "_" + params["name"], **target)[/code]
Il y a plusieurs choses à noter.
• D'abord le fxSDK contient un module Python `fxconv` qui fournit quelques fonctions utilitaires et l'indispensable fonction `elf` dont je reparle dans un instant. Votre boulot se réduit (essentiellement) à lire le fichier d'entrée et à produire des objets `bytes()` avec les octets de la variable finale, ici la `struct map`.
• D'abord il y a un module `fxconv` qui fournit quelques fonctions utilitaires et l'indispensable fonction `elf` dont je reparle dans un instant. Votre boulot se réduit (essentiellement) à lire le fichier d'entrée et à produire un objets `bytes()` avec les octets de la variable finale, ici la `struct map`.
• La fonction `convert` est appelée avec 4 paramètres : `input` et `output` sont les noms du fichier d'entrée et du fichier de sortie, respectivement. `params` est un dictionnaire avec les paramètres indiqués dans `fxconv-metadata.txt` (ainsi que `"type"` indiquant le type de la conversion, et `"name"` indiquant le nom de la variable à créer), et `target` indique la calculatrice cible (Graph mono ou Graph 90+E), le compilateur à utiliser, et d'autres détails. `params["name"]` est toujours défini même si vous avez mis `name_regex` (la regex est appliquée plus tôt).
• La fonction `convert` est appelée avec 4 paramètres : `input` et `output` sont les noms du fichier d'entrée et du fichier de sortie, respectivement. `params` est un dictionnaire avec les paramètres indiqués dans `project.cfg` (plus quelques paramètres toujours présents), et `target` indique la calculatrice cible (Graph mono ou Graph 90+E), le compilateur à utiliser, et d'autres détails.
• `params["custom-type"]` est le type personnalisé spécifié dans `fxconv-metadata.txt`. J'aime bien faire un if/else ici et avoir une sous-fonction par type de données pour ne pas tout mélanger. Si vous reconnaissez le type, vous devez renvoyer 0 ; sinon, vous devez renvoyer 1 auquel cas fxconv tentera le convertisseur personnalisé suivant (et si tous renvoient 1, il indiquera que le type est inconnu).
• `params["type"]` est le nom du sous-dossier dont l'asset est tiré. Si ce nom est `img`, `fonts` ou `bin`, [brown][b]`fxconv`[/b][/brown] utilise ses convertisseurs par défaut, sinon il appelle `converters.py`. J'aime bien faire un if/else ici et avoir une sous-fonction par type de données pour ne pas tout mélanger.
• À la fin de la génération, la fonction `fxconv.elf()` est utilisée pour générer le fichier de sortie. Sauf si vous faites des trucs très extravagants, les trois derniers paramètres ne changeront jamais, et vous n'aurez besoin de `output` et `target` que là. Avec ces simplifications en tête, finalement le procédé de conversion prend en entrée `input` (fichier d'entrée) et `params` (infos de `fxconv-metadata.txt)` et doit générer `data`. ;)
• À la fin de la génération, la fonction `fxconv.elf()` est utilisée pour générer le fichier de sortie. Sauf si vous faites des trucs très extravagants, les trois derniers paramètres ne changeront jamais, et vous n'aurez besoin de `output` et `target` que là. Avec ces simplifications en tête, finalement le procédé de conversion prend en entrée `input` (fichier d'entrée) et `params` (infos de `project.cfg)` et doit générer `data`. ;)
Ici vous pouvez voir que les octets que je génère sont juste ceux du texte `"<Placeholder>"` encodé en ASCII. Ça n'a rien à voir avec une `struct map`, c'était juste pour garder le code court pendant l'explication. Maintenant au passe à la conversion à proprement parler ! ^^
@ -200,19 +195,19 @@ Ici vous pouvez voir que les octets que je génère sont juste ceux du texte `"<
fxconv.elf(o, output, "_" + params["name"], **target)[/code]
C'est un peu long, mais il y a de tout dans cette conversion donc c'est une bonne référence. On va donc prendre le temps de tout bien détailler. :)
On commence par lire le fichier source `input`. Ici c'est un fichier texte, je le lis directement avec `open()`. Quand c'est une image, vous pouvez utiliser `PIL` pour la charger directement et avoir un joli objet idiomatique Python pour faire toutes les opérations dont vous avez besoin. Si vous voulez des exemples de ça, il en y en a plusieurs dans [url=https://gitea.planet-casio.com/Lephenixnoir/fxsdk/src/branch/master/fxconv/fxconv.py]les sources de fxconv[/url] (le plus simple étant `convert_libimg_fx()`).
On commence par lire le fichier source `input`. Ici c'est un fichier texte, je le lis directement avec `open()`. Quand c'est une image, vous pouvez utiliser `PIL` pour la charger directement et avoir un joli objet idiomatique Python pour faire toutes les opérations dont vous avez besoin. Si vous voulez des exemples de ça, il en y en a plusieurs dans [url=https://gitea.planet-casio.com/Lephenixnoir/fxsdk/src/branch/master/fxconv/fxconv.py]les sources de [b]`fxconv`[/b][/url] (le plus simple étant `convert_libimg_fx()`).
La suite est spécifique à mon format de map. Un ligne blanche ("`\n\n"`) sépare les contenus de la map (`tiles`) des infos sur les cycles de portes (`cycle_texts`), et je sépare encore les deux en lignes. Je devine la taille de la map à partir du texte saisi, et pour l'instant je fixe `fog` à 0. Je récupère dans le nom de fichier (`"lv1.txt"`) la partie sans extension ("`lv1"`) comme ça je pourrai automatiquement mettre un pointeur vers l'image du même nom (qui sera "`img_lv1`").
La suite est spécifique à mon format de map. Un ligne blanche ("`\n\n"`) sépare les contenus de la map (`tiles`) des infos sur les cycles de portes (`cycle_texts`), et je sépare encore les deux en lignes. Je devine la taille de la map à partir des tiles qui sont saisies, et pour l'instant je fixe `fog` à 0. Je récupère dans le nom de fichier (`"lv1.txt"`) la partie sans extension ("`lv1"`) comme ça je pourrai automatiquement mettre un pointeur vers l'image du même nom (qui sera "`img_lv1`").
Enfin arrive la conversion de la map à proprement parler. Vous pouvez voir que je commence avec un tableau de `w*h` octets, qui sont tous 0 (`TILE_AIR`) initialement. Ensuite je remplis en lisant les caractères de chaque ligne. Je note les clés 0...9, les portes verticales a...h et les portes horizontales A...H.
Enfin arrive la conversion de la map à proprement parler. Vous pouvez voir que je commence avec un tableau de `w*h` octets, qui sont tous 0 (`TILE_AIR`) initialement. Ensuite je remplis en lisant les caractères de chaque ligne. Je note les clés 0...9, les portes verticales a...z et les portes horizontales A...Z.
Si je tombe sur quelque chose d'imprévu, je lève une exception de type `fxconv.FxconvError`, que fxconv affiche à l'écran avant de renvoyer une erreur au Makefile. Je vous conseille de vraiment blinder vos conversions contre les imprévus. Si une erreur se glisse mais n'est pas détectée, l'objet converti sera inutilisable mais vous n'en serez pas averti·e. C'est comme si votre programme contient une erreur mais le compilateur le laisse passer sans rien dire : le résultat n'aura pas de sens et il sera très difficile de réaliser que le problème vient de là.
Si je tombe sur quelque chose d'imprévu, je lève une exception de type `fxconv.FxconvError`, qui s'affiche à l'écran et arrête la compilation. Je vous conseille de vraiment blinder vos conversions contre les imprévus. Si une erreur se glisse mais n'est pas détectée c'est comme si votre programme contient une erreur mais le compilateur ne s'en rend pas compte : le résultat n'aura pas de sens et il sera très difficile de réaliser que le problème vient de là.
L'étape suivante consiste à analyser le texte des cycles pour obtenir une représentation structurée. Si vous ne connaissez pas les regex ce n'est pas grave, vous pouvez admettre qu'à la fin `cycles` est un dictionnaire du type `{"a": b"#.", "A": b"#."}`. Tout ou presque est en `bytes` au lieu de chaînes de caractères parce que c'est ce dont on aura besoi lors de la génération du fichier de sortie à la fin de la fonction. ^^
L'étape suivante consiste à analyser le texte des cycles pour obtenir une représentation structurée. Si vous ne connaissez pas les regex ce n'est pas grave, vous pouvez admettre qu'à la fin `cycles` est un dictionnaire du type `{"a": b"#.", "A": b"#."}`. Tout ou presque est en `bytes` au lieu de chaînes de caractères parce que tout doit être des `bytes` lors de la génération du fichier converti. ^^
La troisième étape consiste à encoder ces informations en une paire de variables `door_cycle` et `door_cycle_index` de la structure. En gros `door_cycle` c'est la concaténation de tous les cycles ensemble avec un espace entre chaque (ici `b"#. #. "`), et `door_cycle_index` indique où commence chaque cycle (ici celui de `"a"` commence à la position 0 et celui de `"A"` commence à la position 3). Ce format est pratique parce que toutes les informations tiennent dans deux variables de taille fixe.
Remarquez que dans la structure, j'ai annoncé que `door_cycle` ferait 128 octets et `door_cycle_index` 16 octets, donc je suis obligé de tenir parole et respecter ces tailles. Dans le premier cas, j'ai une ligne qui rajoute des 0 à `door_cycle` pour amener la taille à 128. Dans le second cas, je crée le tableau directement avec 16 octets dans le constructeur `bytearray(16)` et ensuite je ne touche plus à la taille.
Remarquez que dans la structure, `door_cycle` fait 128 octets et `door_cycle_index` fait 16 octets donc je suis obligé de respecter ces tailles. Dans le premier cas, j'ai une ligne qui rajoute des 0 à `door_cycle` pour amener la taille à 128. Dans le second cas, je crée le tableau directement avec 16 octets dans le constructeur `bytearray(16)` et ensuite je ne touche plus à la taille.
Ensuite, on génère la map. Ce code-là est super important donc je vous le remets ici. ;)
@ -235,17 +230,17 @@ Le problème c'est que notre structure contient des pointeurs :
• Il y a un pointeur `img` vers une variable externe.
• Il y a un pointeur `tiles` vers un tableau qu'on veut générer en même temps que la structure.
Et ça on ne peut pas connaître leur valeur durant la conversion (l'explication complète de ce problème est détaillée dans la prochaine section). Il y a un mécanisme pour délayer leur calcul, mais c'est un peu compliqué et donc fxconv fournit une classe `fxconv.ObjectData()` qui fait tout ça automatiquement. Pour créer une structure qui contient des pointeurs, vous commencez par créer un objet vide type `ObjectData()`, et ensuite vous pouvez lui ajouter successivement des composants de trois types avec `+` ou `+=` :
Et ça on ne peut pas connaître leur valeur durant la conversion (l'explication complète dans la prochaine section). Il y a un mécanisme pour délayer leur calcul, mais c'est un peu compliqué et donc [brown][b]`fxconv`[/b][/brown] fournit une classe `fxconv.ObjectData()` qui fait tout ça automatiquement. Pour créer une structure qui contient des pointeurs, vous commencez par créer un objet vide type `ObjectData()`, et ensuite vous pouvez lui ajouter successivement des composants.
[b]Octets fixes.[/b] Si vous ajoutez un objet de type `bytes` ou `bytearray`, les octets sont ajoutés directement au résultat de la conversion. Par exemple cette ligne crée les trois premiers `int` de la `struct map`.
[code]o += fxconv.u32(w) + fxconv.u32(h) + fxconv.u32(fog)[/code]
Pour créer des entiers non-signés, le module `fxconv` fournit trois fonctions `fxconv.u8`, `fxconv.u16` et `fxconv.u32`. Elles génèrent respectivement des entiers de 8 bits (1 octet, aka. `char` ou `uint8_t`), 16 bits (2 octets, aka. `short` ou `uint16_t`) et 32 bits (4 octets, aka. `int` ou `uint32_t`). Si vous n'êtes pas familier·ère avec ces histoires de types entiers de taille variable, vous pouvez voir le [url=https://www.planet-casio.com/Fr/forums/topic16574-1-tdm-18-comprendre-les-donnees-et-leurs-representations.html]TDM 18[/url] qui aborde précisément ce sujet.
Il y a trois fonctions `fxconv.u8`, `fxconv.u16` et `fxconv.u32` pour créer des entiers non-signés de 1, 2 et 4 octets (ce qui correspond aux types `char`, `short` et `int`, ou si vous préférez les types de `<stdint.h>`, `uint8_t`, `uint16_t` et `uint32_t`). Si vous n'êtes pas familier·e avec cets types entiers et leurs tailles, vous pouvez voir le [url=https://www.planet-casio.com/Fr/forums/topic16574-1-tdm-18-comprendre-les-donnees-et-leurs-representations.html]TDM 18[/url] qui aborde précisément ce sujet.
[b]Pointeurs vers des variables externes.[/b] Si vous ajoutez `ref("variable")`, un pointeur vers la variable est ajouté au résultat de la conversion. Un pointeur fait toujours 4 octets. Par exemple cette ligne ajoute un pointeur vers l'image de fond (ici c'est `"img_lv1"`).
[b]Pointeurs vers des variables externes.[/b] Si vous ajoutez `ref("variable")`, un pointeur vers la variable est ajouté au résultat de la conversion. Un pointeur fait toujours 4 octets. Par exemple cette ligne ajoute un pointeurs vers l'image de fond (ici c'est `"img_lv1"`).
[code]o += fxconv.ref(f"img_{filename}")[/code]
La commande exacte est `ref(<variable> [,offset=<offset>])`, ça ajoute un pointeur vers `(void*)&variable + offset`. Attention l'offset est en octets et pas en taille d'objets comme en C.
La commande exacte est `ref(<variable> [,offset=<offset>])`, ça ajoute un pointeur vers `(void*)&variable + offset`. L'offset est en octets (et pas en taille d'objets comme en C !).
[b]Pointeurs vers des données annexes.[/b] Si vous ajoutez `ref(octets)` où `octets` est soit un `bytes` soit un `bytearray`, alors les octets sont ajoutés en annexe du résultat et un pointeur vers le résultat est ajouté à la structure. Par exemple, cette ligne ajoute les octets de `encoded_tiles` en annexe de la `struct map` et un pointeur vers cette annexe à la structure.
@ -254,15 +249,6 @@ La commande complète est `ref(<bytes_like> [,padding=<padding>])`. Si vous spé
Et ensuite, on termine avec `fxconv.elf()` qui déplie tout, génère les octets et les références pour les pointeurs, et produit le fichier de sortie avec la structure nouvellement convertie. :D
Remarquez que du coup la map référence `img_lv1` donc il faut fournir `assets-fx/img/lv1.png` sinon l'édition des liens échoue puisqu'il lui manque des variables. ;)
Ce système de conversion est largement supérieur à simplement déclarer des variables avec les contenus dans le code, pour plusieurs raisons ! ^^
• Si vous éditez à la main, vous avez toute la liberté de choisir un format agréable à saisir sans compromettre l'utilisation d'une format optimisé dans le code. Par exemple mon stockage des cycles de portes serait horrible à faire à la main.
• Si vous utilisez un éditeur de niveaux maison, il est très dur (sinon impossible) de lire un niveau à partir de sa `struct map` écrite en C, donc en fait il est probable qui vous ayez déjà un format à vous. Dans ce cas fxconv s'intègre plus facilement à la chaîne de compilation qu'un générateur de code C, et le convertisseur Python est généralement plus simple ou plus court.
• Vous pouvez utiliser des éditeurs externes comme [url=https://www.mapeditor.org/]Tiled[/url] ou [url=https://www.aseprite.org/]Aseprite[/url] si vous prenez le temps d'analyser leur format de fichier. (Est-ce que fxconv supportera directement ces formats un jour ? Peut-être. ;) )
• Pour toute modification, il suffit d'enregistrer le fichier texte décrivant la map, recompiler, et tout est automatique. Si vous aviez l'habitude de Sprite Coder ça élimine totalement ce genre d'étape intermédiaire.
[brown][b]Parenthèse technique : pourquoi gérer les pointeurs différemment ?[/b][/brown]
On vient de voir que les pointeurs doivent être gérés spécialement, et qu'on ne peut pas connaître leur valeur du tout pendant la conversion.
@ -280,19 +266,17 @@ Lorsque ce fichier est compilé, le compilateur génère les octets qui serviron
Pour résoudre ce problème, le compilateur utilise un mécanisme de [i]références[/i] conçu spécifiquement pour ça. Le compilateur génère les octets 00 00 00 00 pour `px` et 00 00 00 08 pour `px8`, et note quelque part dans le fichier `.o` (le résultat de la compilation) que ces deux valeurs sont [i]relatives à `&x`[/i]. L'éditeur de liens, lorsqu'il choisit l'adresse dans la RAM où `x` sera chargé, ajoute `&x` aux deux valeurs, qui deviennent alors correctes.
La capacité de l'éditeur de liens à modifier à la volée des valeurs [i]relatives à des adresses de variables[/i] est très puissante et permet sur un ordinateur de compiler efficacement des programmes immenses, d'avoir des bibliothèques dynamiques, et même de charger du code à l'exécution (par exemple des mods pour votre jeu préféré). ^^
La capacité de l'éditeur de liens de modifier à la volée des valeurs [i]relatives à des adresses de variables[/i] est très puissante et permet sur un ordinateur de compiler efficacement des programmes immenses, d'avoir des bibliothèques dynamiques, et même de charger du code à l'exécution (par exemple des mods pour votre jeu préféré). ^^
Je pense que vous voyez où je veux en venir. Durant notre conversion de map, on ne spécifie pas nous-mêmes les valeurs des pointeurs `img` et `tiles`, on se contente de dire que c'est des valeurs relatives à des adresses de variables, et l'éditeur de liens se charge de déterminer la valeur pour nous bien plus tard juste avant de produire le fichier g1a/g3a. :)
J'ai brièvement mentionné que le compilateur ajoute à son fichier de sortie (un fichier objet `.o`) une sorte d'« annotation » indiquant quelles valeurs sont relatives et relatives à qui. fxconv produit aussi des fichiers `.o` (durant l'appel à `fxconv.elf` ; ELF est le nom du format des fichiers `.o`), et bénéficie donc de ce mécanisme.
J'ai brièvement mentionné que le compilateur ajoute à son fichier de sortie (un fichier objet `.o`) une sorte d'« annotation » indiquant quelles valeurs sont relatives et relatives à qui. [brown][b]`fxconv`[/b][/brown] produit aussi des fichiers `.o` (durant l'appel à `fxconv.elf` ; ELF est le nom du format des fichiers `.o`), et bénéficie donc de ce mécanismes.
Par contre, créer les annotations c'est laborieux surtout à la main. Du coup fxconv produit une description en assembleur de la structure et la fait assembler sur-le-champ, puisque `as` sait très bien produire toutes les annotations. Donc en fait ce que `ObjectData()` vous cache c'est qu'il génère du texte assembleur pour référencer les symboles, et ensuite `fxconv.elf()` invoque l'assembleur pour produire les annotations. ^^
Notez que vous pouvez toujours joindre du code assembleur au résultat de la conversion vous-même (notamment pour générer des symboles annexes ou véritablement du code exécutable) en donnant une valeur au paramètre optionnel `assembly` de `fxconv.elf()`.
Par contre, créer les annotations c'est laborieux surtout à la main. Du coup [brown][b]`fxconv`[/b][/brown] génère un morceau de code assembleur et l'assemble immédiatement, parce que l'assembleur `as` sait très bien produire ce genre d'annotations. Donc en fait ce que `ObjectData()` vous cachez c'est qu'il génère du texte assembleur pour référencer les symboles, et ensuite `fxconv.elf()` invoque l'assembleur pour produire les annotations. ^^
[brown][b]Utilisation de la map dans le programme[/b][/brown]
Maintenant qu'on a des `struct map` complètes et un premier niveau disponible dans le programme, on peut modifier le moteur pour exploiter toutes les nouvelles données. Au début de la fonction `engine_draw()`, je rajoute un `dimage()` pour afficher l'image de fond du niveau :
On peut tout de suite intégrer les nouvelles informations dans le moteur du programme. Au début de la fonction `engine_draw()`, je rajoute un `dimage()` pour afficher l'image de fond du niveau :
[code]void engine_draw(struct game const *game)
{
@ -323,92 +307,26 @@ struct game game = {
Et voilà, on peut maintenant se déplacer sur la map et les collisions sont fonctionnelles ! :D
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-03-colliding-map.gif[/img]
[i]Image 3 : Toujours pas de gameplay mais pour ma défense on s'en approche.[/i][/center]
Vous pouvez consulter le code à ce stade au niveau du [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/commit/80167969fe39de1536af094e6db8e642a8b3718e]commit `80167969f` sur le dépôt[/url].
[i]Image 2 : Toujours pas de gameplay mais pour ma défense on s'en approche.[/i][/center]
[brown][b]Animation des tiles sur la map[/b][/brown]
Dans cette partie j'ai surtout codé le jeu sans apporter beaucoup de modifications spécifiques à gint. La principale modification a été d'ajouter dans la `struct game` décrivant la partie en cours un tableau d'informations dynamiques de tiles. Comme la `struct map` qui est issue de la conversion est en lecture seule (comme tout objet issu de la conversion, par défaut), il est hors de question de la modifier. Donc toutes les infos dynamiques sont dans la `struct game`.
Là j'ai surtout codé le jeu sans apporter beaucoup de modifications. Le système d'animations que j'ai construit n'est pas lié spécifiquement au joueur et peut être utilisé pour à peu près n'importe quoi.
Pour implémenter les mécaniques et gérer les animations, j'ai défini un concept de « tile dynamique ». Ce sont des tiles qui peuvent changer d'état pendant le jeu. Les données d'une tile dynamique comprennent leur position sur le map, leur état actuel, et une animation.
Pour coder les mécaniques et gérer les animations, j'ai ajoutés dans la `struct game` un tableau d'informations dynamiques de tiles ; chaque tile qui peut changer d'état pendant le jeu a une entrée avec sa position, son état actuel et son animation. Au chargement du niveau, des infos dynamiques de tiles sont créées pour l'entrée, la sortie, les portes et les clés, avec les animations qui vont bien. ;)
[code]/* struct dynamic_tile: Dynamic tile information */
struct dynamic_tile
{
/* Position */
int x, y;
/* Current state */
int state;
int state2;
/* Whether animation is idle */
int idle;
/* Current animation */
struct anim_data anim;
};[/code]
Le système d'animations que j'utilise pour le joueur fonctionne tout à fait pour les tiles aussi, en fait il décrit simplement des transitions entre des images de différentes durées. Pour ceux que ça intéresse, voilà comment ça se passe :
Les animations pour l'entrée et la sortie n'ont qu'une phase et émettent des sortes de particules régulièrement dans un sens qui suggère le rôle de la case.
[code]/* struct anim_data: Data for currently-running animations */
struct anim_data
{
/* Animation update function */
anim_function_t *function;
/* Frame to draw */
struct anim_frame img;
/* On-screen entity displacement */
int dx, dy;
/* Animation direction */
int dir;
/* Current frame */
int frame;
/* Duration left until next frame; updated by the engine. Animation
function is called when it becomes negative or null */
int duration;
};[/code]
Une animation contient essentiellement une référence à l'image en cours (`img`) qui est une sous-image d'une spritesheet ou tilesheet, des paramètres d'affichage et de direction, le numéro du frame actuel de l'animation (`frame`) et la durée restante avant le prochain frame (`duration`).
Les animations pour les portes sont un peu plus élaborées. Il y a quatre phases : porte fermée, porte en cours d'ouverture, porte ouverte, porte en cours de fermeture (et pareil pour les portes horizontales). La première et la troisième sont statiques (elles bouclent sur un unique frame), les deux autres affichent une succession de frames puis transitionnent vers leur phase statique naturelle.
À chaque update le moteur du jeu réduit `duration` de la durée écoulée depuis l'update précédente (dans mon cas c'est fixé à 25 ms) et quand `duration` tombe à 0 ou en-dessous de 0, la fonction d'animation `function` est appelée pour passer au frame suivant.
On peut regarder par exemple la fonction d'animation de la tile de début de niveau.
[code]int anim_tile_start(struct anim_data *data, int init)
{
data->function = anim_tile_start;
data->frame = init ? 0 : (data->frame + 1) % 5;
data->duration = (data->frame == 0 ? 1000 : 150);
data->img = anim_frame(&anim_tiles, data->frame, 0);
return 0;
}[/code]
Cette fonction commence par indiquer qui devra être appelé pour la prochaine update dans `data->function` (elle-même) et le prochain frame à afficher (le premier quand on initialise, le suivant modulo 5 sinon). On en déduit la durée (qui ici est 1 seconde pour le frame initial est 150 ms pour les autres) et enfin l'image à afficher (`anim_frame()` sert juste à extraire une sous-image dans la tilesheet).
Dans le cas d'une animation comme l'ouverture de porte, lorsqu'on atteint le frame final on veut faire une transition vers l'animation pour les portes qui sont ouvertes et à l'arrêt, et dans ce cas [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/commit/c2178fd2b03041ff7535d446ecbb4c37ff2be2e4/src/animation.c#L150]il y a une condition[/url] qui teste si on a atteint la fin de l'animation d'ouverture et appelle la fonction d'animation des portes ouvertes à l'arrêt quand ça se produit.
Tout ça donc marche aussi bien pour le joueur que pour les tiles. Ce n'est probablement pas la meilleure manière d'implémenter des animations, mais c'est relativement flexible et ça évite au moteur d'animations de s'inscruter dans le moteur de jeu. ^^
Revenons aux tiles dynamiques du coup. Au chargement du niveau, des tiles dynamiques sont crées pour l'entrée, la sortie, les portes et le clés. Les animations pour l'entrée et la sortie tournent en boucle. Les animations des portes sont séparées en deux fixes (porte ouverte et porte fermée) et deux transitoires (porte en cours d'ouverture et portes en court de fermeture).
L'état des tiles dynamiques est utilisé pour stocker la progression de chaque porte dans son cycle d'ouverture/fermeture, et indiquer le cas particulier où une porte est bloquée en position ouverte par un joueur (si le joueur entre juste avant qu'elle n'essaie de se fermer).
L'état dynamique est utilisé pour stocker la progression de chaque porte dans son cycle d'ouverture/fermetur et indiquer le cas particulier où une porte est bloquée par un joueur (si le joueur entre juste avant qu'elle n'essaie de se fermer).
[brown][b]Premiers éléments de gameplay[/b][/brown]
À ce stade je pense que le gameplay est clair pour vous. On veut traverser chaque niveau de son entrée vers sa sortie, mais il y a des portes partout et elles passent leur temps à s'ouvrir et se fermer. Et donc on tourne en rond en essayant de comprendre les cycles. :E
Pour implémenter réellement le gameplay il suffit de faire avancer chaque porte d'une étape dans son cycle à la fin de chaque tour, et d'empêcher le joueur de se déplacer sur une case où il y a une porte fermée.
Pour implémenter réellement le gameplay il suffit de faire avancer chaque porte d'une étape dans son cycle à la fin de chaque tour, et d'empêcher le jour de se déplacer sur une case où il y a une porte fermée.
Et voici, chers lecteurs, le premier niveau de [b]Mystère noir et blanc[/b] ! :D
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-03-level-1.gif[/img]
[i]Image 4 : Gamer focus activated![/i][/center]
Vous pouvez jouer à ce niveau sur toutes les plateformes supportées par gint (pour un add-in de cette taille, normalement toutes les Graph mono) en [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/commit/c2178fd2b03041ff7535d446ecbb4c37ff2be2e4/MystNB.g1a]téléchargeant `MystNB.g1a` sur le dépôt[/url] (lien direct) au niveau du [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/commit/c2178fd2b03041ff7535d446ecbb4c37ff2be2e4] commit `c2178fd2b`[/url]. :)
[brown][b]Conclusion[/b][/brown]
Ce tutoriel présente toutes les notions nécessaires à la conversion d'assets spécifiques pour un jeu avec fxconv. En plus de la map qu'on a convertie ici, on peut convertir des descriptions d'objets, des séquences de dialogues, des cinématiques, des listes de quêtes, des traductions de textes, et tous autres assets spécifiques à des applications et qui peuvent être représentés et édités dans des formes plus agréables que du code.
Voilà ce qu'on a abordé dans cet épisode. :)
• Comment étendre fxconv avec des conversions personnalisées en Python.
• Comment convertir un objet en une structure définie en C.
• L'utilisation de `fxconv.ObjectData()` pour générer des structures contenant des pointeurs.
• Et une petite discussion sur l'implémentation des éléments dynamiques de Mystère noir et blanc.
[i]Image 3 : Si je vous spoile la solution y'a plus vraiment de mystère ![/i][/center]