fogón

← BibliotecaKitmormon/starter-fps

Kit de inicio: disparos en primera persona

m@mormonKit, MIT
v1hace 2 días

Obra original de Kenney ↗

Clonar
FOGONfogon pieces add starter-fps
GITgit clone https://git.fogon.gg/mormon/starter-fps.git
REMIXfogon new starter-fps --from starter-fps
ZIPstarter-fps.zipBajar
Controles

Kit de inicio: disparos en primera persona

El Starter Kit FPS de Kenney, que es un proyecto de Godot, pasado a TypeScript y three.js para Fogón: la misma mecánica, los mismos números y el mismo nivel. Un personaje que camina, salta dos veces y dispara; dos armas con su cadencia, su dispersión y su retroceso; enemigos voladores que te apuntan y te disparan de cerca; y unas islas flotando en el cielo.

Un enemigo disparando de cerca

En la demo: W A S D para moverse, espacio para saltar (se puede una segunda vez en el aire), clic para disparar y E (o el botón del medio) para cambiar de arma. El mouse queda adentro del juego; Esc lo suelta. Si el navegador no deja atraparlo, se mira arrastrando. En el teléfono: una palanca a la izquierda, se mira arrastrando el resto de la pantalla, y tres botones. También anda con un mando. Si te caés o te quedás sin vida, el nivel empieza de nuevo.

Es solo la página. No hay sala ni red: todo pasa en el navegador de cada uno. server/room.ts es la sala vacía que Fogón le pide a cualquier proyecto.

Qué es de quién

  • El kit original (el código en GDScript y el armado del nivel) es de Kenney, con licencia MIT: su texto está en [LICENSE-kenney.md](LICENSE-kenney.md).
  • Los modelos, los sprites y los sonidos (public/fps/) son de Kenney y están en dominio público (CC0).
  • El port a TypeScript es nuestro, también MIT. La fuente del marcador es Lilita One, que se carga de Google Fonts.

Los archivos

Todo lo que se reusa está en client/fps/. client/main.ts es solo la demo: lee el teclado, el mouse, los dedos y el mando, y dibuja el marcador.

ArchivoQué tieneEn el kit de Godot
game.tsGame: arma el nivel, el jugador, los enemigos y las nubes; update(dt, input) y render(renderer)scenes/main.tscn
player.tsPlayer y PLAYER (velocidad, saltos, gravedad, tamaño): caminar, saltar, mirar, disparar, el retroceso, cambiar de armaobjects/player.gd, player.tscn
weapons.tsWeapon, BLASTER, REPEATER, WEAPONSscripts/weapon.gd, weapons/*.tres
enemy.tsEnemy y ENEMY (vida, daño, alcance, cada cuánto dispara)objects/enemy.gd, enemy.tscn
level.tsLEVEL (el nivel como datos), buildLevel, buildSun, Cloud, at()scenes/main.tscn, objects/cloud.gd
collide.tsSolids: los triángulos del nivel, un rayo contra ellos y un cuerpo que resbala por elloslo hace el motor de Godot
sprites.tsFlash: un sprite que pasa unos cuadros y se apaga (el fogonazo, la marca del disparo)AnimatedSprite3D, objects/impact.gd
audio.tsSounds: la cola de sonidos y los pasosscripts/audio.gd
look.tsapplyLook(renderer), el cielo y la luz de ambientescenes/main-environment.tres
assets.tsloadAssets(), model(): baja los modelos, sprites y sonidosla carpeta del proyecto
index.tsexporta todo lo anterior

Cómo se usa en un juego

fogon pieces add starter-fps

Eso trae client/fps/ y agrega three.js al package.json. Los modelos, sprites y sonidos (menos de 1 MB) no viajan con el juego: se cargan de la demo publicada de esta pieza (https://play.fogon.page/starter-fps/fps/).

import * as THREE from "three";
import { applyLook, Game, loadAssets, noInput } from "./fps";

const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
applyLook(renderer); // sombras y el mismo tono que en Godot

const game = new Game(await loadAssets());
game.onHealth = (health) => (label.textContent = `${health}%`);
game.onWeapon = (weapon) => (crosshair.src = `${game.assets.base}sprites/${weapon.crosshair}`);
game.onEnemyDestroyed = (left) => console.log(`quedan ${left}`);
game.resize(innerWidth / innerHeight);

// Los sonidos arrancan con el primer gesto de la persona.
addEventListener("pointerdown", () => game.sounds?.unlock(), { once: true });

let last = performance.now();
renderer.setAnimationLoop((now) => {
  const input = noInput();
  input.moveY = keys.has("KeyW") ? -1 : 0; // adelante es −1
  input.lookX = mouseX; // píxeles que se movió el mouse desde el cuadro anterior
  input.lookY = mouseY;
  input.shoot = mouseDown; // mientras esté apretado
  input.jump = spacePressed; // solo el cuadro en que se apretó
  input.toggle = ePressed;
  game.update((now - last) / 1000, input);
  game.render(renderer);
  last = now;
});

client/main.ts tiene eso mismo completo, con mouse atrapado, dedos y mando. Para servir los archivos desde tu juego, copiá public/fps/ de esta pieza a tu public/ y cargalos con loadAssets({ base: "./fps/" }).

Otra arma

Un arma son números. Copiá una y cambiale lo que quieras:

import { BLASTER, Game, WEAPONS, type Weapon } from "./fps";

const cannon: Weapon = { ...BLASTER, cooldown: 0.8, shotCount: 5, spread: 2, damage: 40, knockback: 50 };
const game = new Game(assets, { weapons: [...WEAPONS, cannon] });

cooldown son los segundos entre disparos, shotCount los rayos por disparo, spread cuánto se abren (en metros, al final de su alcance), maxDistance el alcance, damage lo que saca cada rayo, knockback cuánto te empuja para atrás y minKnockback/maxKnockback cuánto salta la vista (radianes: arriba, al costado).

Otro nivel

LEVEL es una lista de piezas, cada una con su modelo y su lugar. Los lugares están escritos como los escribe Godot en un .tscn (Transform3D(…): doce números), así que un nivel armado en el editor de Godot se copia tal cual; para escribirlos a mano, at(x, y, z, giro):

import { at, Game, LEVEL, type Level } from "./fps";

const level: Level = {
  ...LEVEL,
  start: [0, 0.5, 0],
  pieces: [
    { model: "platform-large-grass", transform: at(0, -0.5, 0) },
    { model: "platform", transform: at(4, 0.5, -3, 45) },
    { model: "wall-high", transform: at(-1, 0, -2) },
  ],
  enemies: [[4, 3, -3]],
  clouds: [],
};
const game = new Game(assets, { level });

Todo lo de pieces es sólido (se pisa y frena los disparos) salvo que diga solid: false. Los modelos que hay: platform, platform-large-grass, wall-low, wall-high, grass, grass-small, cloud, enemy-flying, blaster y blaster-repeater. Para usar otros, agregalos a la escena (game.scene.add(...)) y, si tienen que ser sólidos, a game.solids.add(objeto).

Los números del jugador y de los enemigos

PLAYER y ENEMY son objetos comunes: PLAYER.speed = 7, PLAYER.jumps = 3, ENEMY.reach = 8, antes de crear el Game.

Lo que quedó distinto del original

  • Sin brillo ni oclusión ambiental. El kit prende el glow y el SSAO de Godot; acá no están. El tono (el «Filmic» de Godot), el cielo, la luz de ambiente y la sombra del sol sí.
  • El retroceso no depende de los cuadros por segundo. En el kit el empujón del arma dura un cuadro, así que empuja más cuanto más lento anda el juego; acá empuja siempre lo que empuja a 60 cuadros por segundo. Lo mismo el giro con el mando.
  • Las colisiones son propias: el cuerpo son tres esferas que se empujan fuera de los triángulos del nivel. Alcanza para pisos, paredes y bordes; no es un motor de física.
  • La mira va en el centro exacto. En el kit queda corrida unos píxeles por cómo está armado el marcador.
  • El cielo es el mismo, pasado de PNG (3,9 MB) a JPG (0,4 MB).

Límites

  • Un solo jugador. Para hacerlo entre varios hace falta una sala que decida quién le pegó a quién.
  • Los enemigos no se mueven de su lugar ni vuelven: cuando no queda ninguno, no pasa nada más. Es una base.
  • Los sonidos son .ogg, como en el kit: los Safari viejos no los reproducen, y ahí el juego anda mudo.

Licencia

MIT (el código, de Kenney y nuestro). Modelos, sprites y sonidos: CC0, de Kenney.