Nichts leichter als das: In der Klasse der Spielfigur fragen Sie in der act()-Methode ab, ob eine Taste gedrückt wurde ( if(Greenfoot.isKeyDown("a")) … ). Wenn ja, dann erzeuge eine neues Objekt der Klasse Geschoss, setze es auf die Welt:

if(Greenfoot.isKeyDown("a"))
    var Geschoss meinGeschoss = new Geschoss()
    this.getWorld().addObject(meinGeschoss, this.getX(), this.getY())

Wie das geht, erfahren Sie hier: Neue Objekte auf der Welt platzieren.

Wir setzen das Geschoss an die Stelle, an der sich der Spieler befindet: this.getX() ist seine x-Position, this.getY() seine y-Position.

Jetzt steht das Geschoss vor dem Spieler und rührt sich nicht. Also braucht die Klasse Geschoss noch eine ordentliche act()-Methode:

public void act()
    this.move(10)
    if(this.isAtEdge())
        this.getWorld().removeObject(this)

Die if-Abfrage sorgt dafür, dass das Geschoss verschwindet, wenn es am Rand der Welt (isAtEdge() == true) ankommt.

Geschosse kontrolliert abschießen

Da die act()-Methode in der Regel ziemlich häufig durchlaufen wird, erscheinen auch auf einen sehr kurzen Tastendruck gleich mehrere Geschosse, da Greenfoot.isKeyDown(...) bei jedem act()-Zyklus aufs Neue überprüft, ob die Taste gedrückt ist und dann true zurückgibt. Da der act()-Zyklus bei Standard-Geschwindigkeitseinstellung mehrere Male pro Sekunde durchlaufen wird, erhalten wir mit einem einzigen kurzen Tastendruck gleich mehrere Geschosse.

1. Möglichkeit: getKey() benutzen

Dies ist die einfachste Möglichkeit, es gibt aber ein paar Fallstricke; ich würde Ihnen raten, grundsätzlich erst mal die zweite Möglichkeit (Timer) zu benutzen.

Die getKey()-Methode gibt uns die Taste zurück, die zuletzt gedrückt wurde, seit die getKey()-Methode zuletzt aufgerufen wurde. Wenn keine Taste gedrückt wurde, wird null zurückgegeben.

Wenn wir also die Space-Taste drücken und festhalten, würden wir erwarten, dass im ersten act()-Zyklus "space" zurückgegeben wird, in allen folgenden act()-Zyklen nur noch null. Das funktioniert aber in Abhängigkeit von der Szenario-Geschwindigkeit nicht immer erwartungsgemäß.

Für eine Szenario-Geschwindigkeit von 50 (Standard-Wert) könnten wir schreiben:

if("space".equals(Greenfoot.getKey())
    this.getWorld().addObject(new Bullet(), this.getX(), this.getY())

(Anmerkung: if(Greenfoot.getKey().equals("space")) führt zu einer Fehlermeldung, wenn Greenfoot.getKey() == null ist; beim Starten eines Szenarios dürfte das immer der Fall sein, da ja zuletzt keine Taste gedrückt wurde. Also immer zuerst die Taste schreiben.)

Tipp zur Fehlervermeidung: getKey() in Variable speichern

Wenn die getKey()-Methode innerhalb eines act()-Zyklus mehrmals aufgerufen wird, gibt nur die erste die gedrückte Taste zurück; alle folgenden getKey()-Methoden geben null zurück. Das folgende Beispiel würde also nicht funktionieren:

if("left".equals(Greenfoot.getKey())
    this.move(-3)
if("right".equals(Greenfoot.getKey())  // ab hier gibt getKey() immer null zurück
    this.move(3)

Die Lösung: Man speichert den Wert von getKey() in einer String-Variablen und benutzt im Folgenden diese:

var String taste = Greenfoot.getKey()
if("left".equals(taste)
    this.move(-3)
if("right".equals(taste)
    this.move(3)

Die getKey()-Methode wird nur einmal aufgerufen.

Ein ähnliches Problem bekommt man, wenn zwei Spieler parallel Tasten bedienen. In diesem Fall holt man die zuletzt gedrückte Taste ebenfalls wie oben als Variable, allerdings macht man das in der act()-Methode der Weltklasse. Diese Variable (am besten ein Attribut der Weltklasse) kann man dann in den Actor-Klassen holen und verwenden.

2. Möglichkeit: Timer - Mit Zählervariable arbeiten

Sie definieren eine Zählervariable, am besten als Attribut unter "Fields":

private int timer = 0

In jedem act()-Zyklus erhöhen Sie den Timer um eins; wenn der Timer einen bestimmten Wert erreicht hat, wird ein Geschoss abgefeuert und der Timer wird auf 0 gesetzt. Je höher der Wert der Zählervariablen, desto größer die Abstände zwischen den Geschossen:

public void act()
    this.timer = this.timer + 1 // oder this.timer++
    if(Greenfoot.isKeyDown("space") && this.timer > 30)
        this.timer = 0 // alles beginnt wieder von vorne
        // Hier ein neues Geschoss abfeuern

Kontrolliertes Dauerfeuer

Über den Timer können Sie die Geschwindigkeit des Dauerfeuers kontrollieren. Wenn der Timer bei 1 "auslöst", dann wird alle zwei act()-Zyklen ein Schuss abgegeben.

KEIN Dauerfeuer: Ein Tastendruck = ein Geschoss

Wenn wir in den obigen Beispielen die Taste festhalten, dann entsteht etwas wie Dauerfeuer. Wenn wir das nicht wollen, schreiben wir etwas wie:

// Attribut:
private boolean leertasteGedrueckt = false

if(!leertasteGedrueckt && Greenfoot.isKeyDown("space"))
    this.getWorld().addObject(new Geschoss(), this.getX(), this.getY())
    leertasteGedrueckt = true
if(leertasteGedrueckt && !Greenfoot.isKeyDown("space"))
    leertasteGedrueckt = false;

Ein Schuss kann nur abgefeuert werden, wenn leertasteGedrueckt == false ist. So müssen wir die Leertaste immer wieder loslassen, damit wir diesen Status erreichen.