Skip to main content Início Criadores johannesrabauer fxgl-skills fxgl-bullet-hell
fxgl-bullet-hell Build a bullet hell (danmaku) game in FXGL — create a tiny precise hitbox independent of the player sprite, add a graze sensor for near-miss scoring, script spiral and circular aimed bullet patterns, implement phase-based boss attack scripting, add a screen- clearing bomb that removes all enemy bullets, handle invincibility frames on hit, accelerate and decelerate bullets mid-flight, create homing bullets with delayed lock- on, and efficiently manage 1000+ simultaneous bullets on screen. Use this skill when building a danmaku shooter, pattern-based boss, or any game requiring dense bullet patterns and precise hitbox design.
Ir para a instalação Skills Marketplace Descubra e explore skills de IA criadas pela comunidade.
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Copiar promptMostrar detalhes do prompt Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
npx skills add https://github.com/JohannesRabauer/fxgl-skills --skill fxgl-bullet-hellO comando permanece em uma só linha. Role horizontalmente para revisá-lo antes de copiar.
Prefere uma cópia local? Baixe os arquivos disponíveis atualmente no SkillsMP.
Baixar Zip Baixando... Add particle systems and visual effects to an FXGL game — configure ParticleEmitter with function-based setters (setVelocityFunction, setAccelerationFunction, setExpireFunction, setScaleFunction, setSpawnPointFunction); use built-in factory emitters (fire, explosion, smoke, rain); apply image-textured particles with multiplyColor / toColor; write custom per-particle physics via setControl; attach TrailParticleComponent for motion trails; apply SlowTimeEffect for bullet-time; apply WobbleEffect for screen-shake; stack multiple effects with EffectComponent; build fireworks displays. Use this skill when adding explosions, fire, smoke, rain, motion trails, impact bursts, bullet-time slow-motion, screen shake, or any particle-based visual polish.
Ocupações relacionadas SOC
Baseado na classificação ocupacional SOC
name fxgl-bullet-hell description Build a bullet hell (danmaku) game in FXGL — create a tiny precise hitbox independent of the player sprite, add a graze sensor for near-miss scoring, script spiral and circular aimed bullet patterns, implement phase-based boss attack scripting, add a screen- clearing bomb that removes all enemy bullets, handle invincibility frames on hit, accelerate and decelerate bullets mid-flight, create homing bullets with delayed lock- on, and efficiently manage 1000+ simultaneous bullets on screen. Use this skill when building a danmaku shooter, pattern-based boss, or any game requiring dense bullet patterns and precise hitbox design.
triggers ["bullet hell","danmaku","spiral pattern","bullet pattern","graze","bomb","hitbox smaller than sprite","homing bullet","1000 bullets","dense bullets","boss pattern"] compatibility Java 17+, FXGL 21.x
category fxgl/game-types tags ["fxgl","java","javafx","game-types","bullet","hell"] metadata {"author":"fxgl-skills","version":"1.0","fxgl-version":"21.1"} allowed-tools ["Read","Write","Edit","Bash"]
FXGL Bullet Hell
Player: Tiny Hitbox + Graze Sensor
@Spawns("player")
public Entity newPlayer (SpawnData data) {
PhysicsComponent hitboxPhysics = new PhysicsComponent ();
hitboxPhysics.setBodyType(BodyType.DYNAMIC);
hitboxPhysics.setFixedRotation(true );
FixtureDef hitFD = new FixtureDef ();
hitFD.setFriction(0 );
hitFD.setDensity(1 );
hitboxPhysics.addFixtureDef(hitFD);
Entity player = entityBuilder(data)
.type(EntityType.PLAYER)
.view("player_ship.png" )
.bbox(BoundingShape.circle(4 ))
.with(hitboxPhysics)
.with(new CollidableComponent (true ))
.with(new BulletHellPlayerComponent ())
.build();
return player;
}
private Entity grazeEntity;
@Override
protected void initGame () {
grazeEntity = entityBuilder()
.type(EntityType.GRAZE_SENSOR)
.at(player.getX(), player.getY())
.bbox(BoundingShape.circle(18 ))
.with( ( ))
.buildAndAttach();
}
{
grazeEntity.setPosition(player.getPosition());
}
new
CollidableComponent
true
@Override
protected
void
onUpdate
(double tpf)
Player Movement and Focused Mode public class BulletHellPlayerComponent extends Component {
private static final double NORMAL_SPEED = 250.0 ;
private static final double FOCUSED_SPEED = 100.0 ;
@Override
public void onUpdate (double tpf) {
boolean focused = getInput().isHeld(KeyCode.SHIFT);
double speed = focused ? FOCUSED_SPEED : NORMAL_SPEED;
double dx = 0 , dy = 0 ;
if (getInput().isHeld(KeyCode.LEFT)) dx -= 1 ;
if (getInput().isHeld(KeyCode.RIGHT)) dx += 1 ;
if (getInput().isHeld(KeyCode.UP)) dy -= 1 ;
if (getInput().isHeld(KeyCode.DOWN)) dy += 1 ;
if (dx != 0 && dy != 0 ) { dx *= 0.7071 ; dy *= 0.7071 ; }
entity.getComponent(PhysicsComponent.class).setVelocityX(dx * speed);
entity.getComponent(PhysicsComponent.class).setVelocityY(dy * speed);
double px = Math.max(FIELD_X, Math.min(FIELD_X + FIELD_W - 8 , entity.getX()));
double py = Math.max(FIELD_Y, Math.min(FIELD_Y + FIELD_H - 8 , entity.getY()));
entity.setPosition(px, py);
}
}
Collision Handlers @Override
protected void initPhysics () {
onCollisionBegin(EntityType.PLAYER, EntityType.ENEMY_BULLET, (player, bullet) -> {
if (getb("invincible" )) return ;
bullet.removeFromWorld();
onPlayerDeath();
});
Set<Entity> grazedBullets = new HashSet <>();
onCollisionBegin(EntityType.GRAZE_SENSOR, EntityType.ENEMY_BULLET, (sensor, bullet) -> {
if (!grazedBullets.contains(bullet)) {
grazedBullets.add(bullet);
inc("grazeScore" , 10 );
inc("bombCharge" , 1 );
play("sounds/graze.wav" );
}
});
onCollisionEnd(EntityType.GRAZE_SENSOR, EntityType.ENEMY_BULLET, (sensor, bullet) ->
grazedBullets.remove(bullet));
}
Spiral Pattern public class SpiralShooterComponent extends Component {
private double angle = 0 ;
private double timer = 0 ;
private static final double FIRE_INTERVAL = 0.05 ;
private static final int ARMS = 3 ;
@Override
public void onUpdate (double tpf) {
timer += tpf;
if (timer < FIRE_INTERVAL) return ;
timer = 0 ;
for (int arm = 0 ; arm < ARMS; arm++) {
double armAngle = angle + arm * (360.0 / ARMS);
double rad = Math.toRadians(armAngle);
Point2D dir = new Point2D (Math.cos(rad), Math.sin(rad));
spawnEnemyBullet(entity.getCenter(), dir, 180 );
}
angle = (angle + 3 ) % 360 ;
}
}
Aimed Pattern + Delay public class AimedBurstComponent extends Component {
private double burstTimer = 0 ;
private static final double BURST_INTERVAL = 2.0 ;
private static final int BURST_COUNT = 5 ;
@Override
public void onUpdate (double tpf) {
burstTimer += tpf;
if (burstTimer < BURST_INTERVAL) return ;
burstTimer = 0 ;
fireAimedBurst();
}
private void fireAimedBurst () {
Point2D playerPos = getGameWorld().getSingleton(EntityType.PLAYER).getCenter();
Point2D dir = playerPos.subtract(entity.getCenter()).normalize();
double baseAngle = Math.toDegrees(Math.atan2(dir.getY(), dir.getX()));
for (int i = 0 ; i < BURST_COUNT; i++) {
int fi = i;
runOnce(() -> {
double rad = Math.toRadians(baseAngle);
spawnEnemyBullet(entity.getCenter(),
new Point2D (Math.cos(rad), Math.sin(rad)), 200 );
}, Duration.millis(i * 80L ));
}
}
}
Homing Bullet (Delayed Lock-On) public class HomingBulletComponent extends Component {
private Point2D velocity;
private double homingDelay;
private double elapsed = 0 ;
private static final double TURN_SPEED = 180 ;
public HomingBulletComponent (Point2D initialDir, double speed, double homingDelay) {
this .velocity = initialDir.multiply(speed);
this .homingDelay = homingDelay;
}
@Override
public void onUpdate (double tpf) {
elapsed += tpf;
if (elapsed > homingDelay) {
Point2D toPlayer = getGameWorld()
.getSingleton(EntityType.PLAYER)
.getCenter()
.subtract(entity.getCenter())
.normalize();
double currentAngle = Math.toDegrees(Math.atan2(velocity.getY(), velocity.getX()));
double targetAngle = Math.toDegrees(Math.atan2(toPlayer.getY(), toPlayer.getX()));
double delta = ((targetAngle - currentAngle + 540 ) % 360 ) - 180 ;
double maxTurn = TURN_SPEED * tpf;
double newAngle = currentAngle + Math.signum(delta) * Math.min(Math.abs(delta), maxTurn);
double speed = velocity.magnitude();
double rad = Math.toRadians(newAngle);
velocity = new Point2D (Math.cos(rad) * speed, Math.sin(rad) * speed);
}
entity.translate(velocity.getX() * tpf, velocity.getY() * tpf);
}
}
Bomb — Screen Clear private void useBomb () {
if (geti("bombs" ) <= 0 ) return ;
inc("bombs" , -1 );
new ArrayList <>(getGameWorld().getEntitiesByType(EntityType.ENEMY_BULLET))
.forEach(Entity::removeFromWorld);
set("invincible" , true );
runOnce(() -> set("invincible" , false ), Duration.seconds(2 ));
Entity flash = entityBuilder()
.view(new Rectangle (getAppWidth(), getAppHeight(), Color.WHITE))
.zIndex(100 )
.buildAndAttach();
animationBuilder()
.duration(Duration.seconds(0.5 ))
.fadeOut(flash)
.onFinished(() -> flash.removeFromWorld())
.buildAndPlay();
play("sounds/bomb.wav" );
}
Invincibility Frames @Override
protected void initGameVars (Map<String, Object> vars) {
vars.put("lives" , 3 );
vars.put("bombs" , 3 );
vars.put("invincible" , false );
vars.put("grazeScore" , 0 );
vars.put("bombCharge" , 0 );
}
private void onPlayerDeath () {
if (geti("lives" ) <= 0 ) {
showGameOver();
return ;
}
inc("lives" , -1 );
inc("bombs" , 1 );
set("invincible" , true );
animationBuilder()
.duration(Duration.millis(100 ))
.repeat(20 )
.autoReverse(true )
.fade(player).from(1.0 ).to(0.1 )
.buildAndPlay();
runOnce(() -> set("invincible" , false ), Duration.seconds(2 ));
}
Performance: Managing 1000+ Bullets
@Spawns("enemyBullet")
public Entity newEnemyBullet (SpawnData data) {
Point2D dir = data.get("dir" );
double speed = data.getOrDefault("speed" , 200.0 );
PhysicsComponent physics = new PhysicsComponent ();
physics.setBodyType(BodyType.DYNAMIC);
physics.setFixedRotation(true );
FixtureDef fd = new FixtureDef ();
fd.setSensor(true );
physics.addFixtureDef(fd);
return entityBuilder(data)
.type(EntityType.ENEMY_BULLET)
.view(new Circle (3 , Color.CRIMSON))
.bbox(BoundingShape.circle(3 ))
.with(physics)
.with(new CollidableComponent (true ))
.with(new BulletMovementComponent (dir, speed))
.with(new ExpireCleanComponent (Duration.seconds(8 )))
.build();
}
public class BulletMovementComponent extends Component {
private final Point2D dir;
private final double speed;
@Override
public void onUpdate (double tpf) {
entity.translate(dir.getX() * speed * tpf, dir.getY() * speed * tpf);
if (entity.getX() < -20 || entity.getX() > getAppWidth() + 20
|| entity.getY() < -20 || entity.getY() > getAppHeight() + 20 ) {
entity.removeFromWorld();
}
}
}
Boss Phase State Machine public class BossComponent extends Component {
private HPComponent hp;
private int phase = 1 ;
private double attackTimer = 0 ;
@Override
public void onUpdate (double tpf) {
double hpRatio = (double ) hp.getValue() / hp.getMaxValue();
int newPhase = hpRatio > 0.6 ? 1 : hpRatio > 0.3 ? 2 : 3 ;
if (newPhase != phase) {
phase = newPhase;
attackTimer = 0 ;
play("sounds/phase_change.wav" );
}
attackTimer += tpf;
double interval = switch (phase) {
case 1 -> 2.5 ;
case 2 -> 1.5 ;
case 3 -> 0.8 ;
default -> 2.5 ;
};
if (attackTimer >= interval) {
attackTimer = 0 ;
switch (phase) {
case 1 -> fireAimedBurst();
case 2 -> { fireSpiral(); fireAimedBurst(); }
case 3 -> { fireSpiral(); fireRing(24 ); }
}
}
}
}
Gotchas
Sensor bodies for bullets, not solid bodies — solid bullets at high density interact
with each other's physics, causing unpredictable velocity changes. Sensors have overlap
detection only with zero physics response.
Player hitbox must be independent of view — FXGL uses the bbox for physics and
collision, NOT the view image size. A 4px-radius circle bbox on a 48px sprite gives proper
bullet hell hitboxes.
BulletMovementComponent is faster than ProjectileComponent at scale — ProjectileComponent
uses Box2D velocity. A custom translate in onUpdate is cheaper for 1000+ bullets.
Graze sensor must track which bullets were already grazed — onCollisionBegin fires
every frame the contact persists in FXGL. Use a Set<Entity> to deduplicate.
new ArrayList<>() before removing from world — calling entity.removeFromWorld()
while iterating getEntitiesByType() causes ConcurrentModificationException. Always copy first.
Bomb clears bullets in next frame — bullets destroyed this frame still process one more
onUpdate and collision check. The invincibility window prevents the player dying in that gap.
Boss movement should use setPosition not physics velocity — boss patterns are typically
scripted (sine wave, figure-8) and are most predictable when position is computed analytically.