19 años en Internet

13 junio 2026

Presentando el Mode9, una librería que extiende DIV2 Games Studios para cargar objetos y mundos 3D en formato OBJ


    Hoy os presento un nuevo capítulo sobre DIV2 Games Studio, un entorno de programación de videojuegos para MS-DOS lanzado en 1999 (y su precuela un año antes) que fue bastante popular en España, en buena parte porque se vendía en los quioscos a un precio muy asequible. Pero el de hoy no es un capítulo cualquiera: Es, probablemente, el proyecto más ambicioso que he traído al blog hasta la fecha.
 
    Como sabéis, DIV2 traía de serie una colección de "modos" de pantalla. Por un lado teníamos el modo 7, que abatía un plano y dibujaba sprites escalados por la distancia (truco empleado en juegos como Super Mario Kart); por otro teníamos el modo 8, que permitía moverse por mapas de sectores planos (`.WLD`) que simulaban un entorno tridimensional, al estilo de los FPS pre-3D (como el primer Doom). Y ya está, eso era lo más parecido a las 3D que podíamos hacer hasta la fecha en DIV Games Studio: Nada de rampas, pendientes, personajes poligonales, etc.
 
    Siempre me quedé con la espinita de hasta dónde se podría empujar el motor… y de ahí ha salido esto: una DLL que añade a DIV2 un nuevo modo que he bautizado como Modo 9, capaz de cargar y recorrer escenarios y objetos tridimensionales de verdad, modelados, por ejemplo, en Blender y exportados como ficheros OBJ. Y podéis descargarla libremente desde mi GitHub e incluso ojear su código fuente (https://github.com/LeHamsterRuso/Mode9/tree/main/BIN).
 

¿Qué es exactamente el Modo 9?

   Podéis ver el Modo 9 como el hermano mayor del modo 8. Donde el modo 8 te daba sectores planos y en un formato propietario (propiedad de Hammer Technologies), el modo 9 te deja cargar cualquier tipo de malla 3D: paredes, suelos, techos y rampas con cualquier inclinación. La cámara es un proceso normal de DIV2 (lo mueves con `advance()`, lo giras cambiando su `angle`, igual que en el modo 7 y 8), así que puedes ponerla en primera persona, en tercera persona siguiendo al personaje, en vista cenital o donde se te ocurra.
 
    Y sobre ese escenario 3D puedes colocar tus personajes de tres maneras:
  • Como billboards planos (sprites 2D que se escalan con la distancia y siempre miran a la cámara, igual que el modo 7). Perfectos para antorchas, árboles o carteles.
  • Como billboards con perspectiva (objetos con una serie de sprites donde el gráfico cambia según el ángulo). Perfecto para personajes (es por ejemplo el tipo de billboards que vemos en juegos como Daggerfall, Doom o el Super Mario Kart de SNES).
  • Como objetos 3D de verdad, mallas OBJ que puedes rodear y ver desde todos los ángulos, con soporte incluso para animación por vértices (vertex morphing).
    Por debajo lleva todo lo que cabe esperar de un motor de la época: gráficos que recuerdan al 3D de la época de la PSX, niebla por distancia, cielo panorámico de 360º, z-buffer, colisiones con deslizamiento, subida automática de escalones y rampas, downscale opcional (baja la resolución nativa del 3D) para ganar velocidad e incluso split-screen a cuatro cámaras. Todo en una DLL compilada con Watcom C++ 10.6, como mandan los cánones.
 
    Cosas a tener en cuenta:
  1. Cuanto menos polígonos tenga tu escenario y objeto 3D, mejor. Intenta no usar modelos de más de 400 polígonos... la librería lo permite, de hecho permite modelos de hasta 8k caras, pero ten en cuenta que un modelo de varios miles de vértices puede hacer que tu juego vaya terriblemente lento.
  2. Los modelos deben de exportarse con caras trianguladas, forward axis -Z y up axis Y.
  3. Los materiales (texturas) no deben de exceder los 1024x1024 (y a ser posible que tenga un tamaño de potencia de 2, como 128x128, 256x256, 512x512, etc). Además debes de convertirlos a formato MAP a 256 colores (el formato típico de DIV y DIV2).
  4. El cielo panorámico no debe de exceder los 4096 x 2048 píxeles. 
 
 

¿Cómo se usa? El esqueleto mínimo

     Una cosa que no shace falta es declarar un typo de proceso c_m9. Por desgracia este tipo de estructura no se pueden inyectar directamente desde las DLL de DIV2 y toca inicializarla en cada proyecto que use esta librería. Con este ejempo, podemos arrancar un mundo 3D cabe en unas pocas líneas (recuerda copiar la DLL en la carpeta DLL que hay dentro de DIV2):
 
PROGRAM paso1;
IMPORT "MODE9.DLL";
CONST
c_m9 = 200;
GLOBAL
STRUCT m9 z; camera; height; angle; END
BEGIN
load_pal("div2.pal"); 
set_mode(m320x200);
set_fps(60, 0);

LOAD_WLD9("OBJ/CASTLE.OBJ", "MAP/CASTLE.MAP", 100); // escenario
LOAD_SKY9("MAP/SKY.MAP"); // cielo (opcional)
MODE9_STEP(70); // altura de los escalones

camara(); // proceso de cámara
END

PROCESS camara()
BEGIN
x = 0; y = -1500; z = 0;
angle = -90000; // mirando hacia el porton del castillo

m9.camera = id; // Este proceso es la cámara
m9.height = 160; // altura del ojo (uds DIV2)
m9.angle = 0; // pitch (0 = horizonte)

START_MODE9(id, OFFSET m9.z, 0);

LOOP
if (key(_w)) advance(20); end
if (key(_s)) advance(-20); end
if (key(_a)) angle += 4000; end
if (key(_d)) angle -= 4000; end
if (key(_esc)) break; end
FRAME;
END
END

 


 

    Como veis, la sintaxis recuerda mucho al modo 8 y en este ejemplo cargamos como escenario un fichero 3D en formato OBJ, le aplicamos un material por defecto, le aplicamos una textura de cielo panorámico y nos desplazamos por el escenario con los típicos controles de WASD. A partir de ese punto, mover la cámara es exactamente igual que mover cualquier proceso de DIV2. Pero lo mejor es verlo crecer paso a paso, que es justo lo que hacen los siguientes ejemplos.

 

Los ejemplos, uno a uno

    He preparado seis programas (ficheros PRG) que van añadiendo una sola cosa nueva cada vez, de modo que podáis parar en el que queráis sin miedo a romper nada. Todos comparten el mismo escenario: un castillo con su patio, una escalera, un lago alrededor de la isla y un cielo panorámico.
 

MODE9_01.PRG · Un mundo vacío para pasear

   Este fichero es ll punto de partida: Cargamos el castillo con `LOAD_WLD9()`, le asignamos sus tres texturas por material (`MODE9_MATERIAL` para la escalera y el agua, ya que `CASTLE.OBJ` trae tres `usemtl`: castillo, escalera y lago) y arrancamos el render.

    La cámara incluye lo que se espera de un FPS clásico: movimiento con WASD, desplazamiento lateral, ratón para girar y mirar arriba/abajo, salto con gravedad y agacharse. Aquí ya se aprecian las colisiones con deslizamiento y el `MODE9_STEP(70)` subiendo la escalera del patio sin que tengamos que programar nada más.

    Cosas a tener en cuenta: La librería permite desactivar y activar las colisiones, de forma que puedas traspasar paredes o incluso golpearte con el techo. No obstante, la gravedad (el hecho de caer hacia abajo) debe de implementarse en el propio PRG.

    Como apreciaréis, he inundado el código con comentarios explicando qué hace cada nueva función de este modo. Por cierto, si pulsáis multiples veces espacio podréis "volar". Es un bug del PRG que he mantenido porque en cierta forma sirve para explorar y ver el escenario desde el cielo.

//------------------------------------------------------------------------------
// TITLE: MODE9.PRG
// DATE: 06/06/2026
// DESCRIPTION: Ejemplo del "modo 9": desplazamiento por un escenario 3D (.OBJ)
// con camara en primera persona (estilo Doom / Elder Scrolls Arena)
// usando MODE9.DLL.
//
// CONTROLES:
// W / S : avanzar / retroceder (SHIFT = correr)
// A / D : desplazamiento lateral (strafe)
// Raton : girar (horizontal) y mirar (vertical / pitch)
// ESPACIO : saltar
// CTRL : agacharse
// ESC : salir
//
// FICHEROS NECESARIOS:
// CASTLE.OBJ : Escenario 3D (castillo + escalera en el patio + lago isla;
// 3 materiales por 'usemtl': Castle, Stairs, Water).
// MAP/CASTLE.MAP: Textura del castillo (material 0, por defecto).
// MAP/STAIRS.MAP: Textura de los peldaños de la escalera (material 1).
// MAP/WATER.MAP : Textura del lago que rodea la isla (material 2).
// MAP/SKY.MAP : Textura del cielo panorámico (skybox).
//
// SOBRE m9 Y c_m9:
// En DIV/DIV2, una DLL no puede crear STRUCTs ni c_types del lenguaje DIV2,
// asi que aqui se declaran: el STRUCT m9 (mismos campos que m8: z, camera, height, angle)
// y la constante c_m9 = 200.
// La camara se activa con START_MODE9(id,&m9.z,0).
// Los sprites se ponen con ctype = c_m9 y coordenadas de MUNDO (x,y,z) + size
// + graph; el modo 9 los dibuja como billboards escalados por distancia SIN
// llamar a nada cada frame (igual que c_m7 en el modo 7). c_m9 DEBE valer lo
// mismo en el PRG y en la DLL (200 por defecto; cambiable con MODE9_CTYPE).
//------------------------------------------------------------------------------
PROGRAM MODE9_DEMO;

IMPORT "MODE9.DLL";

CONST
SCREEN_X = 320; // Ancho de la resolucion de la pantalla
SCREEN_Y = 200; // Alto de la resolucion de la pantalla
GAME_FPS = 60; // FPS objetivo
ADV_UNIT = 30; // velocidad de paso (uds DIV2)
c_m9 = 200; // c_type "logico" del modo 9 (convencion, ver cabecera)

GLOBAL
STRUCT m9 // STRUCT de camara del modo 9 (mismos campos que m8). &m9.z = base del struct.
z;
camera;
height;
angle;
END
scr_x; // resolucion real (la lee la DLL): raton/camara a
scr_y; // cualquier resolucion, no solo 320x200
BEGIN
// Es probable que los assets tarden en cargar en hardware real.
// Si te salta la expcion 142, puedes decomentar esta linea para ignorarla.
//ignore_error(142);

set_mode(SCREEN_X * 1000 + SCREEN_Y);
set_fps(GAME_FPS, 0);

scr_x = MODE9_SCRW(); // MODE9_SCRW() y MODE9_SCRH() se pueden llamar en cualquier momento, no solo al inicio, por si quieres cambiar la resolucion con set_mode durante el juego.
scr_y = MODE9_SCRH();

load_fpg("FPG/MODE9.FPG"); // cargar el FPG del juego

// --- Cargar el escenario 3D (UNA sola llamada: malla + textura + escala) --
// LOAD_WLD9(obj, textura, unitscale). unitscale = 100 uds DIV2 por unidad
// OBJ. Si la textura no existe (o pones ""), el render cae a flat-shading.
// La textura de LOAD_WLD9 es el material 0 y se aplica por defecto a todos los materiales.
// Si cambias 100 por 200, el escenario será el doble de grande.
// La carga del escenario es bloqueante (hasta que no acabe la acción, no se continúa).
// Si cargas escenarios enormes, puede saltar al excepcion 142 ("El proceso parece bloqueado, esperando FRAME")
LOAD_WLD9("OBJ/CASTLE.OBJ", "MAP/CASTLE.MAP", 100);

// --- MULTIMATERIALES -----------------------------------------------------
// CASTLE.OBJ tiene 3 materiales por 'usemtl' (en orden): 0=Castle, 1=Stairs,
// 2=Water. MODE9_MATERIAL(idx, map) asigna una textura a cada uno. El 0 ya lo
// puso LOAD_WLD9 (CASTLE.MAP); aqui ponemos la escalera y el mar. Si un .MAP
// no existe aun, ese material cae al 0 (CASTLE.MAP) sin fallar.
MODE9_MATERIAL(1, "MAP/STAIRS.MAP"); // peldanos de la escalera del patio
MODE9_MATERIAL(2, "MAP/WATER.MAP"); // agua que rodea la isla

// MODE9_CULL(1) activa el backface culling (no dibujar las caras que se ven por detras; mas rapido).
// Por defecto está activado. Si deseseas desactivarlo haz MODE9_CULL(0);
//MODE9_CULL(0);

// MODE9_FOV cambia el campo de vision horizontal (FOV) en grados.
// El valor por defecto es ~71, que es el FOV "canonico" de los juegos 3D de la época
// Un valor menor (60) hace zoom, y un valor mayor (90 o 110) da un efecto de gran angular.
// Cambiar el FOV puede afectar la sensacion de velocidad y la distorsion de las texturas, asi que ajustalo a tu gusto.
//MODE9_FOV(90);

// MODE9_PERSPECTIVE(1) Corrige gran parte de los defectos gráficos de las texturas, pero impacta mucho en el rendimiento.
// Por defecto esta desactivado (0), lo que da un efecto de distorsion de las texturas.
//MODE9_PERSPECTIVE(1);

// Far clip ("flopping"): no dibujar geometria mas alla de N uds DIV2.
// Por defecto está a 0 (sin limite).
// Si activas niebla con SET_FOG9, se auto-activa el FARCLIP con el valor de 'far' de la niebla (lo que queda tras la niebla es del color de fondo, no se dibuja).
//MODE9_FARCLIP(1000);

// MODE9_COLLISION activa y desactiva las colisiones: con 1, advance/xadvance NO atraviesan las paredes (la distancia
// minima a la pared es el campo 'radius' del proceso camara). Con 0 puedes atravesar paredes.
//MODE9_COLLISION(0);

// MODE9_STEP(INT X) se usa para subir escaleras y rampas.
// El valor es la altura max (uds DIV2) que la camara sube sola. Los muros
// mas bajos NO bloquean (se suben como un peldaño) y la camara se apoya
// en el suelo que tiene debajo (rampas inclinadas, escalones). 0 = desactivado.
// La DLL no aplica GRAVEDAD, la gravedad la tienes que gestionar en el PRG (ver el bloque de salto/gravedad de 'camara');
// Ojo, para que funcione las colisiones deben de estar activadas (no va con MODE9_COLLISION(0)).
MODE9_STEP(70);

// --- Ambiente y niebla (color en rango 0..100, convencion DIV2) ----------
// SET_ENV_COLOR9 fija el color de FONDO y tambien el color de la NIEBLA.
// 0..100 por canal: 100=blanco, 0=negro. SET_FOG9(near,far) funde hacia ese
// color crecientemente con la DISTANCIA: transparente hasta 'near' y opaco en
// 'far' (gradiente suave, como el modo 8). OJO: a diferencia del modo 8
// (0..100), aqui near/far son DISTANCIAS en uds DIV2; lo normal es far ~ 2*near.
//SET_ENV_COLOR9(60, 72, 88); // cielo amarillento, impacta en el color de la niebla
//SET_FOG9(300, 1400); // niebla: near=300 far=1400 (uds DIV2)
// Por defecto la niebla no está activada, pero si la activas luego la puedes
// desactivar con MODE9_FOG_OFF.
//MODE9_FOG_OFF();

// --- Cielo panoramico (skybox) -------------------------------------------
// LOAD_SKY9(map): textura .MAP que envuelve 360 en horizontal; sustituye el
// color de fondo plano (SET_ENV_COLOR9).
// Gira con la camara (yaw) y se desplaza con el pitch.
LOAD_SKY9("MAP/SKY.MAP");

// --- Procesos ------------------------------------------------------------
camara();
write(0, 2, scr_y-9, 0, "WASD+RATON mover ESPACIO salto ESC salir");
write_int(0, 0, 0, 0, OFFSET fps);
END


//------------------------------------------------------------------------------
// Proceso CAMARA: se mueve por el escenario como en el modo 8.
//------------------------------------------------------------------------------
PROCESS camara()
PRIVATE
mouse_x_prev;
eye_base;
fall_v; // velocidad de caida (la gravedad la integra el PRG)
grounded; // 1 si la DLL nos sostiene en un suelo/escalon/rampa
z_fell; // z tras aplicar la gravedad, ANTES de que la DLL ajuste
BEGIN
// Posicion inicial (uds DIV2): x,y = plano del suelo; z = altura
x = 0; // frente al porton (el porton esta en z=-1000)
y = -1500; // fuera de la muralla, mirando hacia el castillo
z = 0;
angle = -90000; // mirando hacia +Z (hacia el porton)

// Estado del modo 9
m9.camera = id;
m9.height = 160; // altura del ojo (1.6 uds OBJ; muralla de 4 de alto)
m9.angle = 0; // pitch (0 = horizonte)
eye_base = m9.height;
radius = 40; // distancia minima a la pared (para MODE9_COLLISION)

// START_MODE9(camara, m9, region) -- misma firma que start_mode8.
// camara = id del proceso camara.
// m9 = &m9.z (la DIRECCION del STRUCT; en modo 8 aqui iria el indice
// de m8, pero al ser m9 un struct del PRG hay que pasar su
// direccion. &m9.z = base del struct; usa &m9[n].z para un array
// de camaras). La DLL lee m9.camera/height/angle de ahi cada
// frame (no hace falta MODE9_SET ni MODE9_BIND aparte).
// region = 0 (pantalla completa; unica region soportada por la DLL).
START_MODE9(id, &m9.z, 0);

//MODE9_DOWNSCALE(2) reduce la resolución del render del modo 9.
// Es un truco empleado en juegos first person shooter de consolas de 16 bits
// Cada valor de downscale reduce la resolucion a 1/2, 1/3 o 1/4 (y duplica lineas para llenar la pantalla).
// 1 = resolucion completa (sin downscale, valor por defecto);
// 2 = render a la mitad de resolucion (si usas 320x200 -> Se aplica 160x100);
// 3 = un tercio (si usas 320x200 -> Se aplica 80x50);
// 4 = un cuarto (si usas 320x200 -> Se aplica 40x25);
// Acelera mucho el render a costa de una gran perdida de calidad (pixelado, texturas borrosas, mas aliasing). Ajustalo a tu gusto.
//MODE9_DOWNSCALE(2);

mouse_x_prev = mouse.x;
fall_v = 0;
grounded = 1;


load_pal("div2.pal");
LOOP
if (key(_esc)) STOP_MODE9(); exit("Bye", 0); end

// --- Movimiento lateral (strafe) ---
if (key(_d)) xadvance(angle - 90000, ADV_UNIT); end
if (key(_a)) xadvance(angle + 90000, ADV_UNIT); end

// --- Avanzar / retroceder ---
if (key(_w))
if (key(_l_shift) or key(_r_shift)) advance(ADV_UNIT*2);
else advance(ADV_UNIT); end
end
if (key(_s)) advance(-ADV_UNIT); end

// --- Salto y gravedad (LA GRAVEDAD VIVE EN EL PRG) -------------------
// El PRG integra la gravedad (z baja cada frame). El SUELO -incluidos
// escalones y rampas- lo pone la DLL TRAS el FRAME: si el jugador se apoya
// en algo, la DLL le SUBE z (ver MODE9_STEP). Por eso despues del FRAME
// comprobamos si z subio respecto a lo que dejo la gravedad: si subio y
// estabamos cayendo, es que tocamos suelo (grounded) y paramos la caida.
if (key(_space) and grounded)
fall_v = -60; // impulso de salto (negativo = hacia arriba)
grounded = 0;
end
fall_v = fall_v + 6; // gravedad (subida)
if (fall_v > 0) fall_v = fall_v + 8; end // caida mas rapida (game feel)
z = z - fall_v; // caer (o subir, si fall_v<0 por el salto)
if (z < 0) z = 0; end // suelo de seguridad en y=0 (por si no hay malla)
z_fell = z; // z tras la gravedad, antes de que la DLL ajuste

// --- Agacharse ---
if (key(_control)) m9.height = eye_base * 2 / 3;
else m9.height = eye_base; end

// --- Raton: giro horizontal y pitch vertical (adaptado a la resolucion) -
// Yaw: sensibilidad ~constante en cualquier modo (256000/scr_x = 800 @320).
angle = angle - (mouse.x - mouse_x_prev) * 256000 / scr_x;
// Pitch: mapeo ABSOLUTO de mouse.y. Centro = scr_y/2 (horizonte real);
// factor 120000/scr_y para que toda la altura cubra +-60000 (=600 @200).
m9.angle = (scr_y/2 - mouse.y) * 120000 / scr_y;
if (m9.angle > 60000) m9.angle = 60000; end
if (m9.angle < -60000) m9.angle = -60000; end

if (mouse.x <= 0 or mouse.x >= scr_x-1) mouse.x = scr_x/2; end
mouse_x_prev = mouse.x;

FRAME;

// Tras el FRAME la DLL ya pudo SUBIR z (escalon/rampa/apoyo en el suelo).
// Si z quedo por encima de lo que dejo la gravedad y veniamos cayendo,
// hemos tocado suelo: marcamos grounded y anulamos la velocidad de caida.
if (z > z_fell - 1 and fall_v > 0)
grounded = 1;
fall_v = 0;
else
grounded = 0;
end
END
END

 

    Y aquí os dejo el ejemplo en movimiento:



 

MODE9_02.PRG · Lo poblamos con billboards


    Idéntico al anterior, pero ahora metemos sprites al estilo modo 7. Y es de lo más cómodo: basta con darle a un proceso `ctype = c_m9` y sus coordenadas de mundo en `x, y, z`, y el Modo 9 lo dibuja solo cada frame, escalado por la distancia, sin que llamemos a nada. Coloco varios árboles y un cartel (billboards planos de una cara) y un NPC con gráficos angulares (`xgraph`): una tabla de 8 dibujos repartidos en 360º, de forma que la DLL elige el frame correcto según desde qué ángulo lo mira la cámara, igual que el `xgraph` del modo 7. El z-buffer se encarga de ocultarlos cuando una pared se interpone.

    En verde el código añadido y con "(...)" las líneas omitidas por ser idénticas al ejemplo anterior: 

PROGRAM MODE9_DEMO;

IMPORT "MODE9.DLL";

CONST
(...)
GLOBAL
(...) 
BEGIN
(...)
// --- Procesos ------------------------------------------------------------
camara();
NPC(0, 600, 0); // un sprite angular "modo 7" en el patio del castillo, representa un NPC
M9TREE(600, -1300, 0); // un sprite plano, representa un arbol
M9TREE(-600, -1300, 0); // un sprite plano, representa un arbol
M9TREE(1000, -1600, 0); // un sprite plano, representa un arbol
M9TREE(-1000, -1600, 0); // un sprite plano, representa un arbol
M9SIGN(300, -1200, 0); // un cartel plano con textura

write(0, 2, scr_y-9, 0, "WASD+RATON mover ESPACIO salto ESC salir");
write_int(0, 0, 0, 0, OFFSET fps);
END
(...)
//------------------------------------------------------------------------------
// Procesos de SPRITE billboard (estilo modo 7): basta con ponerle ctype = c_m9 y
// sus coordenadas de MUNDO (x,y = suelo, z = altura), graph y size. El modo 9
// lo dibuja automaticamente como sprite 2D escalado segun la distancia, SIN
// llamar a nada cada frame (igual que un proceso c_m7 en el modo 7).
// - x, y : posicion en el plano del suelo (uds DIV2)
// - z : altura (uds DIV2)
// - size : escala base en % (la distancia la aplica el modo 9 encima)
//
// OCLUSION: el modo 9 oculta el sprite si queda tapado por la geometria (test
// de z-buffer). Se puede desactivar con MODE9_OCCLUDE(0).
//------------------------------------------------------------------------------
PROCESS NPC(wx, wy, wz)
PRIVATE
npc_gr[] = 8, 8,1,2,3,4,5,6,7; // N=8 graficos en 360º

BEGIN
ctype = c_m9; // <-- el modo 9 lo pinta como billboard automaticamente
//graph = 8; // grafico de MODE9.FPG, billboard plano (1 cara)
xgraph = &npc_gr[0]; // grafico de MODE9.FPG, billboard con multiples caras, en funcion del angulo de vision
size = 100; // escala base (%); el modo 9 la modula por distancia
angle = 0;
x = wx; y = wy; z = wz; // posicion en el MUNDO (el proceso la conserva)
LOOP
FRAME;
END
END

PROCESS M9TREE(wx, wy, wz)
BEGIN
ctype = c_m9; // <-- el modo 9 lo pinta como billboard automaticamente
graph = 9; // grafico de MODE9.FPG, billboard plano (1 cara)
size = 100; // escala base (%); el modo 9 la modula por distancia
angle = 0;
x = wx; y = wy; z = wz; // posicion en el MUNDO (el proceso la conserva)
LOOP
FRAME;
END
END

PROCESS M9SIGN(wx, wy, wz)
BEGIN
ctype = c_m9; // <-- el modo 9 lo pinta como billboard automaticamente
graph = 10; // grafico de MODE9.FPG, billboard plano (1 cara)
size = 100; // escala base (%); el modo 9 la modula por distancia
angle = 0;
x = wx; y = wy; z = wz; // posicion en el MUNDO (el proceso la conserva)
LOOP
FRAME;
END
END


 

    Y aquí lo tenéis en movimiento:

 

    Por cierto, el NPC que véis es un render de un personaje que modelé en Blender en octubre del año pasado (aquí lo podéis ver en verdadero 3D: https://sketchfab.com/3d-models/nazuna-from-backstabbed-in-a-backwater-dungeon-855b836348714abaa321d4638f2358db). Y bueno, sobra decir que los árboles y la señal también los modelé yo mismo, en este caso para un proyecto abandonado que tenía el nombre en código "La Aventura de Greta: Eboke Debe Dimitir" y del que había ya preparado un boceto para su pantalla de "pulsa start":

 



MODE9_03.PRG · Metemos un objeto 3D de verdad en nuestro escenario 3D de verdad


    Cuando un billboard plano no basta —queremos rodear al bicho y verlo por todos lados— y los billboards con sprites por ángulos se nos queda corto, toca meter un modelo 3D. Aquí cargo una gata humanoide con `LOAD_OBJ9("OBJ/CAT.OBJ", "MAP/CAT.MAP", 50)`, que devuelve un handle, y lo dibujo cada frame con `DRAW_OBJ9()` en la entrada del castillo.

    Un detalle importante: los modelos se cargan en segundo plano (mientras jugamos), así que `LOAD_OBJ9` devuelve el handle al instante, pero el gato simplemente "aparece" cuando termina de cargar. A más polígonos, materiales o animaciones, más tardará en cargar. No obstante, he implementado una función de `MODE9_OBJ9_READY() que nos permite consultar si el objeto ligado al manejador ha acabado de cargar, lo cual permite, por ejemplo, la creación de pantallas de carga.

     Al igual que antes, en verde dejo el código añadido y con "(...)" las líneas omitidas por ser idénticas al ejemplo anterior: 

PROGRAM MODE9_DEMO;

IMPORT "MODE9.DLL";

CONST
(...)
GLOBAL
(...)
cat_h; // handle del modelo 3D del gato (LOAD_OBJ9)
BEGIN
(...)
// --- Cielo panoramico (skybox) -------------------------------------------
// LOAD_SKY9(map): textura .MAP que envuelve 360 en horizontal; sustituye el
// color de fondo plano (SET_ENV_COLOR9).
// Gira con la camara (yaw) y se desplaza con el pitch.
LOAD_SKY9("MAP/SKY.MAP");

// --- NPC 3D estatico (objeto OBJ texturado en el mundo) ------------------
// LOAD_OBJ9(obj, textura, unitscale) carga UN modelo 3D estatico: la malla
// CAT.OBJ con su textura CAT.MAP, escala 50 (uds DIV2 por unidad OBJ). Ocupa
// un slot de los 8 de la DLL. El gato se vera quieto (sin animacion); para
// animarlo por vertex morphing se usaria LOAD_OBJ9_ANIM + DRAW_OBJ9_ANIM.
cat_h = LOAD_OBJ9("OBJ/CAT.OBJ", "MAP/CAT.MAP", 50);
/*
// Los modelos se cargan en segundo plano, mientras se ejecuta el juego
// No obstante, con MODE9_OBJ9_READY puedes consultar si el modelo ha
// acabado de cargarse en memoria (1 = listo, 0 = aun cargando).
// Puede servir, por ejemplo, para hacer pantallas de carga
// Esto es opcional, puedes llamar a DRAW_OBJ9 o DRAW_OBJ9_ANIM con un handle aun no cargado (0) y simplemente no dibuja nada en ese FRAME.
// Pero ten en cuenta que, mientras el modelo se carga, el juego se ralentiza
write(0, SCREEN_X/2, SCREEN_Y/2, 4, "Cargando modelo 3D...");
while(MODE9_OBJ9_READY(cat_h) == 0)
FRAME;
END;
delete_text(ALL_TEXT);*/

// --- Procesos ------------------------------------------------------------
camara();
NPC3D(cat_h, 400, -1300, 0);
NPC(0, 600, 0); // un sprite angular "modo 7" en el patio del castillo, representa un NPC
M9TREE(600, -1300, 0); // un sprite plano, representa un arbol
M9TREE(-600, -1300, 0); // un sprite plano, representa un arbol
M9TREE(1000, -1600, 0); // un sprite plano, representa un arbol
M9TREE(-1000, -1600, 0); // un sprite plano, representa un arbol
M9SIGN(300, -1200, 0); // un cartel plano con textura

write(0, 2, scr_y-9, 0, "WASD+RATON mover ESPACIO salto ESC salir");
write_int(0, 0, 0, 0, OFFSET fps);
END
(...)
//------------------------------------------------------------------------------
// NPC3D: objeto 3D texturado en el mundo. Usa DRAW_OBJ9 cada frame para que la
// DLL lo renderice como malla 3D (con z-buffer respecto al escenario).
// handle : devuelto por LOAD_OBJ9 (0 = modelo no cargado -> no dibuja nada)
// wx, wy : posicion inicial en el plano del suelo (uds DIV2)
// wz : altura inicial (uds DIV2; 0 = al nivel del suelo)
//
// El proceso mantiene sus coordenadas (x,y,z,angle) como cualquier proceso DIV2:
// puedes moverlo con advance(), cambiar angle, etc. en subclases.
// DRAW_OBJ9(handle, x, y, z, angle) se llama antes de cada FRAME y dibuja el
// modelo estatico (sin animacion: el gato se queda quieto).
//------------------------------------------------------------------------------
PROCESS NPC3D(handle, wx, wy, wz)
BEGIN
x = wx; y = wy; z = wz;
angle = pi;
LOOP
if (handle > 0)
DRAW_OBJ9(handle, x, y, z, angle);
end
FRAME;
END
END

 

     Y aquí lo tenéis en movimiento:

 

     Sobre la gata humanoide, se trata de una revisión lowpoly (menos de 400 polígonos) de un modelo que hice en Blender en 2021 basándome en el rediseño de Ankha que le hizo Zero Zone (aquí podéis consultar la versión original que hice entonces: https://sketchfab.com/3d-models/ankha-zone-version-37819f223f26467e8b3247e83b507805). Si véis que el modelo lowpoly no se parece mucho al que hice hace 5 años, es lógico... Pensad que he comprimido más de 130.000 triángulos en apenas unos 400.



MODE9_04.PRG · Y ahora, que el objeto esté animado


    El mismo personaje, pero vivo. No es que esté bailando o corriendo, pero le he puesto una pequeña animación de "idle" (espera/estar quieto). Para ello, en lugar de `LOAD_OBJ9` uso `LOAD_OBJ9_ANIM("OBJ/CAT", ...)`, que carga una secuencia de poses numeradas (`CAT0.OBJ` … `CAT9.OBJ`), y lo pinto con `DRAW_OBJ9_ANIM()` pasándole un parámetro extra, `fpos`, que indica en qué punto de la animación está.

    Mi Modo 9 interpola las poses intermedias por nosotros, así que con 10 fotogramas el ciclo de "respiración" del gato queda suave y para ello ligo el `fpos` al `timer` para que la animación corra a la misma velocidad independientemente de los FPS a los que vaya el juego. En otras palabras, los vértices no se teletransportan del punto A al punto B, si no que hacen un recorrido. Esto lo hago porque en hardware modesto es poco realista hacer animaciones grandes de 180 posiciones, por poner un ejemplo... Al igual que en las animaciones de Blender, si capturas la posición de los vértices en el momento 0 y otra vez en el momento 50, con esta librería se recalculan las posiciones entre 1 y 49, para que la animación luzca fluída, sin que le tengas que especificar en cada frame cual es la posición exacta de los vértices.

    Otra cosa a aclarar es que, como los OBJ no tienen soporte nativo para animaciones, los OBJ de CAT1, CAT2, CAT3, CAT4 son idénticos (mismos vértices, UV, materiales, etc), pero con posiciones cambiadas. Esto, si tienes maña con Blender, no es complicado de implementar e incluso te ayuda a exportar las animaciones para funcionar de esta forma.

    Tened en cuenta también de que si pensáis usar animaciones 3D, resultaría recomendable que hagáis uso de MODE9_OBJ9_READY() para implementar una pantalla de carga, puesto que, a pesar de que los objetos se cargan en segundo plano, pueden causar ralentizaciones (pensad que una animación 3D de 10 frames es como cargar en memoria 10 objetos 3D, uno tras otro).

    Al igual que antes, en verde dejo el código modificado y con "(...)" las líneas omitidas por ser idénticas al ejemplo anterior:


PROGRAM MODE9_DEMO;

IMPORT "MODE9.DLL";

CONST
(...)
c_m9 = 200; // c_type "logico" del modo 9 (convencion, ver cabecera)
CAT_FRAMES = 10; // fotogramas del idle del gato (CAT0.OBJ..CAT9.OBJ)
CAT_LOOP = 300; // duracion del bucle en centisegundos (300 = 3 s, ver timer)

GLOBAL
(...)
BEGIN
(...)
// --- NPC 3D animado (objeto OBJ texturado en el mundo) -------------------
// LOAD_OBJ9_ANIM carga una animacion por vertex morphing: los fotogramas son
// OBJ/CAT0.OBJ..OBJ/CAT9.OBJ (misma topologia, solo cambian las posiciones) con una
// unica textura CAT.MAP compartida. Ocupa UN SOLO slot de los 8 de la DLL,
// independientemente del numero de fotogramas. La carga se separa de NPC3D y
// se llama con FRAME entre medias para refrescar antes de que arranquen los
// procesos (y porque parsear 10 OBJ tarda varios segundos en disco real).
cat_h = LOAD_OBJ9_ANIM("OBJ/CAT", "MAP/CAT.MAP", 50, CAT_FRAMES);
/*
// Los modelos se cargan en segundo plano, mientras se ejecuta el juego
// No obstante, con MODE9_OBJ9_READY puedes consultar si el modelo ha
// acabado de cargarse en memoria (1 = listo, 0 = aun cargando).
// Puede servir, por ejemplo, para hacer pantallas de carga
// Esto es opcional, puedes llamar a DRAW_OBJ9 o DRAW_OBJ9_ANIM con un handle aun no cargado (0) y simplemente no dibuja nada en ese FRAME.
// Pero ten en cuenta que, mientras el modelo se carga, el juego se ralentiza
write(0, SCREEN_X/2, SCREEN_Y/2, 4, "Cargando modelo 3D...");
while(MODE9_OBJ9_READY(cat_h) == 0)
FRAME;
END;
delete_text(ALL_TEXT);*/

// --- Procesos ------------------------------------------------------------
camara();
NPC3D(cat_h, 400, -1300, 0);
NPC(0, 600, 0); // un sprite angular "modo 7" en el patio del castillo, representa un NPC
M9TREE(600, -1300, 0); // un sprite plano, representa un arbol
M9TREE(-600, -1300, 0); // un sprite plano, representa un arbol
M9TREE(1000, -1600, 0); // un sprite plano, representa un arbol
M9TREE(-1000, -1600, 0); // un sprite plano, representa un arbol
M9SIGN(300, -1200, 0); // un cartel plano con textura

write(0, 2, scr_y-9, 0, "WASD+RATON mover ESPACIO salto ESC salir");
write_int(0, 0, 0, 0, OFFSET fps);
END
(...)
//------------------------------------------------------------------------------
// NPC3D: objeto 3D texturado en el mundo. Usa DRAW_OBJ9 cada frame para que la
// DLL lo renderice como malla 3D (con z-buffer respecto al escenario).
// handle : devuelto por LOAD_OBJ9 (0 = modelo no cargado -> no dibuja nada)
// wx, wy : posicion inicial en el plano del suelo (uds DIV2)
// wz : altura inicial (uds DIV2; 0 = al nivel del suelo)
//
// El proceso mantiene sus coordenadas (x,y,z,angle) como cualquier proceso DIV2:
// puedes moverlo con advance(), cambiar angle, etc. en subclases.
// DRAW_OBJ9_ANIM(handle, x, y, z, angle, fpos) se llama antes de cada FRAME.
// 'fpos' es la posicion de la animacion en 1/256 de fotograma; aqui se deriva de
// timer[0] (centisegundos) para un bucle continuo de CAT_LOOP cs = 3 s. Para un
// modelo cargado con LOAD_OBJ9 (estatico) DRAW_OBJ9_ANIM tambien vale: la DLL ve
// nframes=1 y dibuja siempre el frame 0, ignorando fpos.
//------------------------------------------------------------------------------
PROCESS NPC3D(handle, wx, wy, wz)
PRIVATE
fpos;
BEGIN
x = wx; y = wy; z = wz;
angle = pi;
LOOP
if (handle > 0)
// bucle de CAT_LOOP cs -> 0..CAT_FRAMES*256; la DLL interpola entre
// keyframes (idle suave) y hace bucle al volver al frame 0.
fpos = (timer[0] % CAT_LOOP) * (CAT_FRAMES * 256) / CAT_LOOP;
DRAW_OBJ9_ANIM(handle, x, y, z, angle, fpos);
end
FRAME;
END
END


 

    Y éste es el resultado en movimiento:

 



MODE9_05.PRG · De un mapa a otro: el castillo por dentro


    Aquí es donde se pone serio. Este ejemplo es ya casi un pequeño juego: tiene pantalla de inicio ("Pulsa una tecla"), una pantall de salida con menú Sí/No, y sobre todo, un cambio de escenario al cruzar una puerta. La clave es que `LOAD_WLD9()` recarga el mundo en caliente, sin parar el Modo 9 ni perder la cámara: detecto que el jugador está sobre la puerta (distancia al cuadrado por debajo de un radio), hago un *fade* a negro para disimular, descargo los props del patio con `signal(..., s_kill)`, cargo el interior (`CASTIN.OBJ`), apago el cielo con `SKY9_OFF()`, bajo la luz ambiente con `SET_ENV_COLOR9()` y recoloco al jugador.

    Dentro hay un NPC (básciamente he mudado a Nazuna aquí) sobre un estrado, un trono, lámparas colgando y antorchas en las paredes, y un bocadillo de diálogo que sigue al personaje en pantalla usando `MODE9_PROJECT()` para proyectar la posición de su cabeza a coordenadas 2D. Y también hay un guiño a cierto consejo sobre lo peligroso que es andar solo.

    Como siempre, en verde dejo el código modificado y con "(...)" las líneas omitidas por ser idénticas al ejemplo anterior:

PROGRAM MODE9_05_DEMO;

IMPORT "MODE9.DLL";

CONST
SCREEN_X = 320; // Ancho de la resolucion de la pantalla
SCREEN_Y = 200; // Alto de la resolucion de la pantalla
GAME_FPS = 60; // FPS objetivo
ADV_UNIT = 30; // velocidad de paso (uds DIV2)
c_m9 = 200; // c_type "logico" del modo 9 (convencion, ver cabecera)

MAP_OUT = 0; // exterior (patio)
MAP_IN = 1; // interior (sala)
SWAP_CD = 40; // frames de cooldown tras un cambio de mapa

// Puerta del torreon (exterior): OBJ z=-4 -> mundo y=-400.
DOOR_X = 0; DOOR_Y = -400; DOOR_R2 = 20000; // radio de disparo ~141
// Puerta de salida (interior): OBJ z=-6 -> mundo y=-600.
EXIT_X = 0; EXIT_Y = -600; EXIT_R2 = 22500; // radio de disparo ~150
// NPC dentro de la sala (mismo sitio que NPC(0,500,50)): globo de dialogo al acercarse.
NPC_X = 0; NPC_Y = 500; BUBBLE_R2 = 160000; // radio del globo ~400
NPC_HEAD = 220; // punto (mundo) a la altura de la CABEZA: ancla del bocadillo y destino del rabito
NPC_TOP = 320; // punto (mundo) SOBRE la cabeza: ahi va el nombre
BUB_HW = 78; // semiancho del bocadillo lateral (1 linea de frase)
BUB_HH = 12; // semialto del bocadillo
BUB_GAP = 12; // separacion entre el NPC y el bocadillo

GLOBAL
STRUCT m9 // STRUCT de camara del modo 9 (mismos campos que m8). &m9.z = base del struct.
z;
camera;
height;
angle;
END
scr_x; // resolucion real (la lee la DLL): raton/camara a
scr_y; // cualquier resolucion, no solo 320x200
map_state; // MAP_OUT / MAP_IN (se muestra como 1 digito arriba-izda)

BEGIN
(...)
LOAD_FPG("FPG/MODE9.FPG");
MODE9_LOADFPG("FPG/MODE9.FPG");

MODE9_STEP(70);

map_state = MAP_OUT;
camara(); // muestra la pantalla de inicio, carga el 3D y juega
END


//------------------------------------------------------------------------------
// CAMARA: movimiento (igual que MODE9_04) + INTERCAMBIO DE MAPAS (todo inline).
//------------------------------------------------------------------------------
PROCESS camara()
PRIVATE
mouse_x_prev;
eye_base;
fall_v; // velocidad de caida (la gravedad la integra el PRG)
grounded; // 1 si la DLL nos sostiene en un suelo/escalon/rampa
z_fell; // z tras aplicar la gravedad, ANTES de que la DLL ajuste
cooldown; // frames restantes sin poder disparar puertas
dx; dy; d2;
bub_on; // 1 = globo de dialogo del NPC visible
bub_fill; bub_brd; bub_tail; // bocadillo: elipse rellena, borde, rabito
bub_spk; // texto de la frase (dentro del bocadillo)
name_plate; name_txt; // placa + nombre, encima del NPC
bd2; // distancia (al cuadrado) de la camara al NPC
show; // 1 = se cumplen las condiciones para ver el globo
nsx; nsy; nsz; // proyeccion de la CABEZA del NPC (MODE9_PROJECT)
tsx; tsy; // proyeccion del punto SOBRE la cabeza (nombre)
cx; cy; tx; // centro del bocadillo y x del rabito en su borde
pk; // id del texto "Pulsa una tecla" (parpadeo)
i; detected; // sondeo de teclas en la pantalla de inicio
sel; ed_cur; // dialogo de salida: opcion elegida (1=Si,0=No) y cursor
BEGIN

// =================== PANTALLA DE INICIO (pulsa una tecla) ===============
// Aun no se ha cargado el mundo 3D: es una pantalla 2D normal.
draw(3, 0, 3, 0, 18, 36, scr_x-18, scr_y-36); // fondo oscuro
draw(2, 15, 15, 0, 18, 36, scr_x-18, scr_y-36); // marco
write(0, scr_x/2, 56, 4, "EL CASTILLO 3D");
write(0, scr_x/2, 74, 4, "( Modo 9 - DIV2 )");
pk = 0;
detected = 0;

// Fondo del titulo (graph 14 = TITLEM9.PCX, 320x200).
put_screen(0, 14);
frame;
// Esperar a que se pulse cualquier tecla. Se sondea key() de todos los
// scancodes (estado real "pulsada"), porque scan_code aqui es un pulso de
// 1 frame y se pierde si el sondeo no cae justo en ese frame.
repeat
if ((timer[0]/40) % 2 == 0)
if (pk == 0)
pk = write(0, scr_x/2, 138, 4, "Pulsa una tecla");
end
else
if (pk <> 0)
delete_text(pk);
pk = 0;
end
end
FRAME;
until(scan_code == 0);
// Esperar a soltar todas las teclas antes de empezar.
repeat
FRAME;
until (scan_code != 0);
delete_text(all_text);
delete_draw(all_drawing);
clear_screen();
FRAME;
// ---------------- CARGA INICIAL: EXTERIOR ----------------
LOAD_WLD9("OBJ/CASTLE.OBJ", "MAP/CASTLE.MAP", 100);
MODE9_MATERIAL(1, "MAP/STAIRS.MAP");
MODE9_MATERIAL(2, "MAP/WATER.MAP");
LOAD_SKY9("MAP/SKY.MAP");

M9TREE(600, -1300, 0); // props del patio (el NPC vive DENTRO, no aqui)
M9TREE(-600, -1300, 0);
M9TREE(1000, -1600, 0);
M9TREE(-1000, -1600, 0);
M9SIGN(300, -1200, 0);

// Posicion inicial (uds DIV2): x,y = plano del suelo; z = altura
x = 0; // frente al porton (el porton esta en z=-1000)
y = -1500; // fuera de la muralla, mirando hacia el castillo
z = 0;
angle = -90000; // mirando hacia +Z (hacia el porton)

(...)
mouse_x_prev = mouse.x;
fall_v = 0;
grounded = 1;
bub_on = 0;

// HUD del juego (se crea aqui, ya pasada la pantalla de inicio).
write(0, scr_x/2, scr_y-9, 4, "WASD+RATON ESPACIO salto ESC salir");
write_int(0, 2, 2, 0, OFFSET fps);

LOOP
// --- ESC: dialogo 2D de confirmacion "Deseas salir? Si/No" ---------
if (key(_esc))
STOP_MODE9(); // pausa el render 3D
put_screen(0, 15);

while (key(_esc)) FRAME; end // soltar ESC
delete_text(all_text); // limpiar HUD/bocadillo
delete_draw(all_drawing);
bub_on = 0;
sel = 0; // 0 = No (por defecto), 1 = Si
draw(3, 0, 3, 0, 40, 68, scr_x-40, 132); // fondo oscuro
draw(2, 15, 15, 0, 40, 68, scr_x-40, 132); // marco del panel
write(0, scr_x/2, 84, 4, "Deseas salir?");
write(0, scr_x/2-34, 112, 4, "Si");
write(0, scr_x/2+34, 112, 4, "No");
ed_cur = draw(2, 14, 15, 0, scr_x/2+18, 104, scr_x/2+50, 122); // cursor en "No"
repeat
if (key(_left)) sel = 1; end
if (key(_right)) sel = 0; end
if (sel == 1) move_draw(ed_cur, 14, 15, scr_x/2-50, 104, scr_x/2-18, 122);
else move_draw(ed_cur, 14, 15, scr_x/2+18, 104, scr_x/2+50, 122); end
FRAME;
until (key(_enter))
if (sel == 1) STOP_MODE9(); exit("Bye", 0); end
// "No": limpiar dialogo, recrear HUD y reanudar el juego 3D.
delete_text(all_text);
delete_draw(all_drawing);
clear_screen();
write(0, scr_x/2, scr_y-9, 4, "WASD+RATON ESPACIO salto ESC salir");
write_int(0, 2, 2, 0, OFFSET fps);
START_MODE9(id, &m9.z, 0);
while (key(_enter)) FRAME; end // soltar intro
mouse_x_prev = mouse.x; // evitar tiron de camara al reanudar
end

// --- Movimiento lateral (strafe) ---
if (key(_d)) xadvance(angle - 90000, ADV_UNIT); end
if (key(_a)) xadvance(angle + 90000, ADV_UNIT); end

(...)
if (mouse.x <= 0 or mouse.x >= scr_x-1) mouse.x = scr_x/2; end
mouse_x_prev = mouse.x;

// ===================== INTERCAMBIO DE MAPAS (INLINE) =================
if (cooldown > 0)
cooldown = cooldown - 1;
else
if (map_state == MAP_OUT)
// Pegados a la puerta del torreon? -> ENTRAR
dx = x - DOOR_X; dy = y - DOOR_Y; d2 = dx*dx + dy*dy;
if (d2 < DOOR_R2)
fade_off();
frame;
fade_on();
frame;
signal(type M9TREE, s_kill); // descargar props del patio
signal(type M9SIGN, s_kill);
// Recarga en caliente: el modo 9 sigue ACTIVO y la camara
// enlazada (id + &m9.z) persiste; LOAD_WLD9 solo cambia la malla.
LOAD_WLD9("OBJ/CASTIN.OBJ", "MAP/CASTIN_T.MAP", 100); // 1 material (0)
SKY9_OFF();
SET_ENV_COLOR9(8, 8, 12);
NPC(0, 500, 50); // el NPC, ahora dentro (sobre el estrado)
THRONE(0, 575, 50); // trono detras del NPC, en el estrado
LAMP(0, 300, 500); // lampara colgando del techo
LAMP(0, -150, 500); // lampara colgando del techo
TORCH( 470, 300, 250); // 4 antorchas en las paredes
TORCH( 470, -100, 250);
TORCH(-470, 300, 250);
TORCH(-470, -100, 250);
x = 0; y = -300; z = 0; angle = -90000;
m9.height = eye_base; m9.angle = 0;
fall_v = 0; grounded = 1;
map_state = MAP_IN;
cooldown = SWAP_CD;
end
else
// Pegados a la puerta de salida? -> SALIR
dx = x - EXIT_X; dy = y - EXIT_Y; d2 = dx*dx + dy*dy;
if (d2 < EXIT_R2)
fade_off();
frame;
fade_on();
frame;
signal(type NPC, s_kill); // descargar NPC y props de la sala
signal(type THRONE, s_kill);
signal(type LAMP, s_kill);
signal(type TORCH, s_kill);
LOAD_WLD9("OBJ/CASTLE.OBJ", "MAP/CASTLE.MAP", 100);
MODE9_MATERIAL(1, "MAP/STAIRS.MAP");
MODE9_MATERIAL(2, "MAP/WATER.MAP");
LOAD_SKY9("MAP/SKY.MAP");
M9TREE(600, -1300, 0);
M9TREE(-600, -1300, 0);
M9TREE(1000, -1600, 0);
M9TREE(-1000, -1600, 0);
M9SIGN(300, -1200, 0);
x = 0; y = -650; z = 0; angle = 90000;
m9.height = eye_base; m9.angle = 0;
fall_v = 0; grounded = 1;
map_state = MAP_OUT;
cooldown = SWAP_CD;
end
end
end

// ===================== NOMBRE + BOCADILLO (estilo comic) ============
// Se muestra SOLO si: estamos en la sala, CERCA del NPC y MIRANDOLO
// (MODE9_PROJECT devuelve 1 = delante de la camara, y su x cae en pantalla).
// - El NOMBRE ("Nazuma") va ENCIMA del NPC (sobre su cabeza), sin tapar
// la cara, con una placa semitransparente para leerse.
// - El BOCADILLO con la frase va a un LADO del NPC; elige IZDA o DCHA
// segun donde haya mas sitio (si el NPC esta en la mitad izquierda de
// la pantalla, el bocadillo va a la derecha, y viceversa). El RABITO
// sale del lado del bocadillo que mira al NPC.
// Ambos SIGUEN al NPC (se reposicionan con move_draw/move_text cada frame).
show = 0;
if (map_state == MAP_IN)
bd2 = (x - NPC_X)*(x - NPC_X) + (y - NPC_Y)*(y - NPC_Y);
if (bd2 < BUBBLE_R2)
if (MODE9_PROJECT(NPC_X, NPC_Y, NPC_HEAD, OFFSET nsx, OFFSET nsy, OFFSET nsz))
if (nsx >= 0 and nsx < scr_x) // el NPC esta en pantalla -> lo miramos
show = 1;
end
end
end
end

if (show)
// Punto sobre la cabeza para el nombre (si no proyecta, usa la cabeza).
if (MODE9_PROJECT(NPC_X, NPC_Y, NPC_TOP, OFFSET tsx, OFFSET tsy, OFFSET nsz) == 0)
tsx = nsx; tsy = nsy - 20;
end
if (tsy < 8) tsy = 8; end // que el nombre no se salga por arriba

// Lado del bocadillo segun el espacio disponible en pantalla.
if (nsx < scr_x/2) cx = nsx + BUB_GAP + BUB_HW; // NPC a la izda -> bocadillo a la DCHA
else cx = nsx - BUB_GAP - BUB_HW; // NPC a la dcha -> bocadillo a la IZDA
end
if (cx < BUB_HW) cx = BUB_HW; end // recortar para no salirse
if (cx > scr_x - BUB_HW) cx = scr_x - BUB_HW; end
cy = nsy; // a la altura de la cabeza
if (cy < BUB_HH + 2) cy = BUB_HH + 2; end
if (cy > scr_y - BUB_HH - 12) cy = scr_y - BUB_HH - 12; end
// Borde del bocadillo que mira al NPC (de ahi sale el rabito).
if (cx > nsx) tx = cx - BUB_HW; else tx = cx + BUB_HW; end

if (bub_on == 0)
// draw(tipo, color, opacidad 0..15, region, x0,y0, x1,y1)
bub_fill = draw(5, 0, 7, 0, cx-BUB_HW, cy-BUB_HH, cx+BUB_HW, cy+BUB_HH); // elipse rellena semitransp.
bub_brd = draw(4, 15, 15, 0, cx-BUB_HW, cy-BUB_HH, cx+BUB_HW, cy+BUB_HH); // borde de la elipse
bub_tail = draw(1, 15, 15, 0, tx, cy, nsx, nsy); // rabito hacia el NPC
bub_spk = write(0, cx, cy, 4, "Es peligroso andar solo");
name_plate = draw(3, 0, 7, 0, tsx-24, tsy-7, tsx+24, tsy+7); // placa del nombre
name_txt = write(0, tsx, tsy, 4, "Nazuma"); // nombre encima del NPC
bub_on = 1;
else
// ya existe: solo reposicionar para seguir al NPC
move_draw(bub_fill, 0, 7, cx-BUB_HW, cy-BUB_HH, cx+BUB_HW, cy+BUB_HH);
move_draw(bub_brd, 15, 15, cx-BUB_HW, cy-BUB_HH, cx+BUB_HW, cy+BUB_HH);
move_draw(bub_tail,15, 15, tx, cy, nsx, nsy);
move_text(bub_spk, cx, cy);
move_draw(name_plate, 0, 7, tsx-24, tsy-7, tsx+24, tsy+7);
move_text(name_txt, tsx, tsy);
end
else
if (bub_on == 1)
delete_draw(bub_fill);
delete_draw(bub_brd);
delete_draw(bub_tail);
delete_draw(name_plate);
delete_text(bub_spk);
delete_text(name_txt);
bub_on = 0;
end
end

FRAME;

// Tras el FRAME la DLL ya pudo SUBIR z (escalon/rampa/apoyo en el suelo).
// Si z quedo por encima de lo que dejo la gravedad y veniamos cayendo,
// hemos tocado suelo: marcamos grounded y anulamos la velocidad de caida.
if (z > z_fell - 1 and fall_v > 0)
grounded = 1;
fall_v = 0;
else
grounded = 0;
end
END
END

//------------------------------------------------------------------------------
// SPRITES billboard (estilo modo 7): ctype = c_m9 + coords de MUNDO + graph/size.
// Se destruyen con signal(type ..., s_kill) al cambiar de mapa.
//------------------------------------------------------------------------------
PROCESS NPC(wx, wy, wz)
PRIVATE
npc_gr[] = 8, 8,1,2,3,4,5,6,7;
BEGIN
ctype = c_m9;
xgraph = &npc_gr[0];
size = 100;
angle = pi/2;
x = wx; y = wy; z = wz;
LOOP
FRAME;
END
END

(...)
PROCESS M9SIGN(wx, wy, wz)
BEGIN
ctype = c_m9;
graph = 10;
size = 100;
angle = 0;
x = wx; y = wy; z = wz;
LOOP
FRAME;
END
END

//------------------------------------------------------------------------------
// Billboards del INTERIOR (graficos id >= 11 en FPG/MODE9.FPG):
// 11 = antorcha, 12 = lampara de techo, 13 = trono.
// Se crean al entrar a la sala y se destruyen con signal(type ...) al salir.
//------------------------------------------------------------------------------
PROCESS TORCH(wx, wy, wz)
BEGIN
ctype = c_m9;
graph = 11;
size = 200;
angle = 0;
x = wx; y = wy; z = wz;
LOOP
FRAME;
END
END

PROCESS LAMP(wx, wy, wz)
BEGIN
ctype = c_m9;
graph = 12;
size = 300;
angle = 0;
x = wx; y = wy; z = wz;
LOOP
FRAME;
END
END

PROCESS THRONE(wx, wy, wz)
BEGIN
ctype = c_m9;
graph = 13;
size = 400;
angle = 0;
x = wx; y = wy; z = wz;
LOOP
FRAME;
END
END


    Y así queda en movimiento:

 


MODE9_MU.PRG · Bonus: pantalla partida a dos jugadores


Para rematar, una demo de split-screen. Este ejemplo lo comentaré simplemente por arriba: Se definen dos regiones (mitad superior y mitad inferior de la pantalla) con `MODE9_REGION`, y se arrancan dos cámaras con sus respectivos struct `m9` mediante `START_MODE9(c1, &m9[0].z, 1)` y `START_MODE9(c2, &m9[1].z, 2)`.

     Cada jugador maneja su cámara (uno con WASD, otro con los cursores) y —detalle bonito— como cada cámara lleva también `ctype = c_m9`, cada jugador ve al otro como un billboard angular (Nazuna para variar) dentro del mundo. Para acabar sólo mencionar que mi Modo 9 admite hasta cuatro vistas simultáneas.

 


     Puedes verlo en movimiento en este tuit: https://x.com/Hamster_ruso/status/2063713220397801517/video/1


La documentación

    Soy consciente de que una librería así no se aprende solo leyendo código, de modo que he generado una ayuda en HTML completa, navegable, con la lista de todas las funciones agrupadas por temas, una guía de "primeros pasos" por etapas (las mismas que los ejemplos), una sección sobre cómo preparar los modelos OBJ y otra con las limitaciones del motor (número máximo de vértices, triángulos, materiales, modelos, billboards, etc.). La tenéis en dos idiomas: `HELP_MODE9_EN` en inglés y `HELP_MODE9_ES` en castellano. Símplemente descargaros el respositorio github del proyecto y abrid el `index.html` de la carpeta que prefiráis.



¿Y ahora qué?


Me hace especial ilusión este capítulo porque siento que el Modo 9 abre la puerta a un tipo de juego que mucha gente daba por imposible en DIV2. Tenéis la DLL ya compilada en la carpeta `BIN`, lista para usar; los ejemplos y los assets en `EXAMPLES`; y el código fuente en `SRC`.

Si esta entrada te ha interesado, te dejo el enlace a los capítulos anteriores de la serie sobre DIV2 Games Studio:

  • Ejemplos sencillos 
  • Programando un JRPG (dificultad avanzada)
  • Programando un bullet-hell (dificultad avanzada)
  • Programando un tamagochi
  • Especial DLL 


  • Y hasta aquí el ejemplo de hoy. ¡Nos leemos!