Kuidas luua dialoogisüsteemi

Sissejuhatus

Mängudes on dialoog vahend, mille kaudu tegelased suhtlevad omavahel ja mängijaga. See võib olla oluline loo edasiandmiseks, maailma süvendamiseks või valikupõhise narratiivi loomiseks.

Näide:

Kui mängija kõnnib NPC juurde, ilmub ekraanile tekst: „Tere tulemast meie külla!“

Mida peaks enne dialoogide lisamist teadma?

Enne dialoogide loomist veendu, et mängul on olemas:

  • Peategelane, keda mängija saab liigutada.

  • NPC (non-player character), kes on ekraanil nähtav ja kellele saab läheneda.

  • Collider-id ehk nähtamatud kokkupõrkealad, mis määravad, millal midagi mängus "juhtub" (nt dialoog algab).

Collider-id ja triggerid

Collider-id on ristkülikukujuline ala mängumaailmas, mida kasutatakse objektide kokkupõrgete või nendevahelise lähedusulatuse tuvastamiseks.

Dialoogisüsteemides kasutatakse collider-eid sageli triggerina (interaktsiooni käivitajana). Näiteks, kui mängija liigub NPC vahetusse lähedusse, tuvastatakse kokkulangevus collider-tega ning dialoog käivitatakse automaatselt või nupuvajutuse abil.

Mängija astub NPC juurde → dialoog käivitub.

Näide kolliiderite loomisest tegelasele ja NPC-le

Rectangle playerCollider = new Rectangle(playerX, playerY, width, height);
Rectangle npcCollider = new Rectangle(npcX, npcY, width, height);

if (npcCollider.overlaps(playerCollider)) {
    // Käivita dialoog
    dialogueManager.startDialogue();
}

Näide projekti koodist:

Antud abimeetodit kasutatakse selleks, et kontrollida, kas dialoog võib alata. Meetod tagastab väärtuse true vaid juhul, kui:

  • mängija ja NPC collider-id kattuvad,

  • dialoog ei ole parasjagu aktiivne,

  • dialoogiaken ei ole nähtaval.

See aitab hoida mänguloogika korras ja võimaldab kontrollida dialoogide aktiveerimise tingimusi ühes kohas.

private boolean canStartDialogue() {
    return npc.getCollider().overlaps(characterCollider) && !inDialogue && !dialogueBox.isVisible();
}

example-dialogue-gif

Videos demonstreeritakse olukorda, kus mängija liigub ekraanil NPC juurde. Kui mängija ja NPC collider-id kattuvad, kuvatakse ekraanile tekst: „Press SPACE to talk“. See annab mängijale märku, et ta võib käivitada dialoogi vajutades tühikuklahvi.

Troubleshooting

Arenduse käigus on sageli vajalik visuaalselt kontrollida, kas collider-id on õigesti määratletud ning kas nad kattuvad ootuspäraselt. Selleks saab kasutada ShapeRenderer klassi, mis võimaldab joonistada collider-id ekraanile erinevates värvides.

Alltoodud koodis punase ristkülikuga tähistatakse mängija collider ning rohelisega NPC collider. See visuaalne kontrollimeetod võimaldab kiiresti tuvastada võimalikke vigu collider-te paigutuses või suurustes.

ShapeRenderer shapeRenderer = new ShapeRenderer();

public void render() {
    shapeRenderer.begin(ShapeRenderer.ShapeType.Line);
    shapeRenderer.setColor(Color.RED);
    shapeRenderer.rect(playerCollider.x, playerCollider.y, playerCollider.width, playerCollider.height);
    shapeRenderer.setColor(Color.GREEN);
    shapeRenderer.rect(npcCollider.x, npcCollider.y, npcCollider.width, npcCollider.height);
    shapeRenderer.end();
}

Dialoogi kuvamine

Dialoogi näitamiseks peame looma dialoogiakna – graafilise elemendi, mis näitab teksti ekraanil.

DialogueBox klass

Kujunda dialoogiaken, kasutades ShapeRenderer -i ja BitmapFont -i abil kastide ja teksti joonistamist.

Koodinäide lihtsast dialoogiaknast:

BitmapFont font = new BitmapFont();
SpriteBatch batch = new SpriteBatch();

public void renderDialogue(String text) {
    batch.begin();
    font.draw(batch, text, 100, 100);
    batch.end();
}

Teksti kuvamine samm-sammult

Dialoogide haldamiseks on soovitatav luua andmestruktuur, mis talletab iga dialoogirea sisu koos kõneleja nimega.

1. Salvesta dialoogirea sisu näiteks DialogueLine klassi:

class DialogueLine {
    String speaker;
    String text;
}

Näide projektist koodist:

Java record tüüp sobib siin hästi, kuna dialoogirida on muutumatu andmestruktuur, mis sisaldab ainult andmeid (ilma loogikata).

public record DialogueLine(String speaker, String text) {}

2. Kuvamine ühe klahvivajutuse kaupa:

if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) {
    dialogueManager.nextLine();
}

Näide projektist koodist:

private void handleDialogue() {
    if (npc.getCollider().overlaps(characterCollider) && !inDialogue && !dialogueBox.isVisible()) {
        if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) {
            inDialogue = true;
            currentDialogueIndex = 0;
            dialogueBox.show(conversation[currentDialogueIndex]);
        }
    } else if (inDialogue && Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) {
        currentDialogueIndex++;
        if (currentDialogueIndex >= conversation.length) {
            dialogueBox.hide();
            inDialogue = false;
        } else {
            dialogueBox.show(conversation[currentDialogueIndex]);
        }
    }
}

Ülaltoodud meetod kontrollib esmalt, kas mängija on NPC-le piisavalt lähedal ja ega dialoog pole juba aktiivne. Kui kõik tingimused on täidetud ja mängija vajutab tühikuklahvi, käivitatakse dialoog ning kuvatakse esimene rida. Iga järgmine vajutus liigub järgmise dialoogirea juurde. Kui kõik read on esitatud, peidetakse dialoogiaken ja dialoogiseisund lõppeb.

3. Mängija liikumise keelamine dialoogi ajal:

if (dialogueManager.isDialogueActive()) {
    player.setMovementEnabled(false);
}

Näide projektist koodist:

Antud sisendikäsitlemise meetodis kontrollitakse, kas dialoog on aktiivne. Kui jah, siis sisendsündmused (nt liikumisklahvid) ei käivita mängija liikumist. See tagab, et dialoogi ajal jääb tegelane paigale ning mängija saab keskenduda dialoogi lugemisele, ilma et tegelane liiguks tahtmatult ekraanil.

private void handleInput() {
    moving = false;
    float delta = Gdx.graphics.getDeltaTime();
    if (!inDialogue) {
        if ((Gdx.input.isKeyPressed(Input.Keys.A) || Gdx.input.isKeyPressed(Input.Keys.LEFT)) && characterPosition.x > 0) {
            characterPosition.x -= SPEED * delta;
            facingLeft = true;
            moving = true;
        } else if ((Gdx.input.isKeyPressed(Input.Keys.D) || Gdx.input.isKeyPressed(Input.Keys.RIGHT))
            && characterPosition.x < SCREEN_WIDTH - CHARACTER_WIDTH) {
            characterPosition.x += SPEED * delta;
            facingLeft = false;
            moving = true;
        }
    }
}

Dialoog koodis või failist?

On kaks võimalust dialoogide kirjutamiseks – valik sõltub sellest, kui palju dialooge on plaanis mängus kasutada.

1. Lihtne lahendus – dialoog otse koodis

Dialoogide kirjutamine otse koodi sobib väiksemate projektide või prototüüpide jaoks, kus dialoogi on vähe. See on kiire lahendus, kuid muutub raskesti hallatavaks, kui dialoogide maht suureneb.

Näiteks selles projektis on ainult üks dialoog, mis on kirjutatud otse koodi:

conversation = new DialogueLine[] {
    new DialogueLine("NPC", "Hello, do you know how to make dialogues with LIBGDX?"),
    new DialogueLine("Character", "Oh yeah, now I know all about that!")
};

Selline lahendus sobib hästi kiireks katsetamiseks või juhul, kui kogu dialoog on teada ja ei muutu projekti käigus.

2. Paindlikum lahendus – dialoog välisfailis (nt JSON-formaadis)

Kui dialooge on rohkem või neid on vaja muuta ilma koodi ümber kompileerimata, on mõistlik hoida need eraldi failis, näiteks JSON-vormingus. See võimaldab paremat struktuuri ja lihtsamat haldust, eriti suuremate mängude puhul.

[
  {"speaker": "NPC", "text": "Tere!"},
  {"speaker": "Mängija", "text": "Tere-tere!"}
]

Koodi näide JSON-faili laadimisest:

Sellise lähenemisega saab dialooge eraldi hallata, näiteks neid tõlkida või muuta ilma vajaduseta koodi muuta. Lisaks võimaldab see kasutada mitmeid dialoogifaile erinevate tasemete, tegelaste või sündmuste jaoks.

Array dialogueLines = json.fromJson(Array.class, DialogueLine.class,
        Gdx.files.internal("dialogues/intro.json"));

Stiil ja kasutusmugavus

Dialoogiaken on oluline kasutajaliidese osa, mis peab olema selgesti loetav, ent samas mitte segama mängu visuaalset kogemust ega varjama olulist infot.

1. Veendu, et dialoogiaken ei kata tegelasi või olulisi mänguelemente.

Dialoogiakna paigutus peab arvestama mängumaailma elementidega. Kui dialoog ilmub otse tegelaste või objektide peale, võib see segada mängija tähelepanu ja vähendada mängitavust.

Siin on näide dialoogist, mis katab tegelasi: dialogue-over-character

2. Kasuta poolläbipaistvat tausta

Läbipaistvus võimaldab mängijal paremini näha taustal toimuvaid tegevusi või positsioneerida end maailmas. Täiesti läbipaistmatu dialoogiaken võib peita olulist visuaalset infot.

Näide dialoogiaknast ilma poolläbipaistvuse efektita:

dialogue-without-transparency

3. Teksti animatsioon – tähtede kaupa ilmumine

Tähtede kaupa ilmuv tekst lisab dialoogile dünaamilisust ning võimaldab paremat tempot loetavusele. Mängijal on võimalik keskenduda tekstile järjest, mitte kogu lõigule korraga.

Kuidas seda teha

Näide on võetud näidisprojektist

Muutujad:

  • typing – kas tekst parajasti ilmub täht-tähelt.

  • displayedText – tekst, mis on seni ekraanile ilmunud.

  • typeTimer – loendab aega tähtede vahel.

  • charIndex – mitmes täht parajasti kuvatakse.

  • CHAR_INTERVAL – kui kiiresti tähed ilmuvad (0.03 sekundit iga tähe kohta).

Mis toimub koodis?

  • Igal kaadril (render) lisatakse möödunud aeg (delta) taimerile.

  • Kui taimer on suurem kui CHAR_INTERVAL, lisatakse järgmine täht.

  • Kui kõik tähed on lisatud, typing = false.

...
private boolean typing = false;
private StringBuilder displayedText = new StringBuilder();
private float typeTimer = 0;
private int charIndex = 0;
private static final float CHAR_INTERVAL = 0.03f; // seconds per character
...
public void render(SpriteBatch batch) {
    ...
    float delta = Gdx.graphics.getDeltaTime();
    if (typing) {
        typeTimer += delta;
        while (typeTimer >= CHAR_INTERVAL && charIndex < currentLine.text().length()) {
            displayedText.append(currentLine.text().charAt(charIndex));
            charIndex++;
            typeTimer -= CHAR_INTERVAL;
        }
        if (charIndex >= currentLine.text().length()) {
            typing = false;
        }
    }
    ...

Näide dialoogist, kus tekst ilmub tähtede kaupa:

typing-animation-gif

4. Portree NPC-st dialoogi juures

Dialoogiakna visuaalne täiendus NPC portreega aitab mängijal paremini seostada kõnelejat konkreetse tegelasega. See suurendab kaasatust ja toetab narratiivi mõistmist.

Kuidas seda teha

Näide on võetud näidisprojektist

Simple2DGame

  • Kontrollitakse, kes räägib (kas "NPC" või "Character").

  • Vastavalt valitakse sobiv pilt (näiteks npcFaceTexture).

  • Antakse DialogueBox -ile nii dialoogirida kui ka pilt, et see oskaks mõlemat näidata.

private void handleDialogue() {
    ...
    Texture texture = conversation[currentDialogueIndex].speaker().equals("NPC") ? npcFaceTexture : characterFaceTexture;
    dialogueBox.startTyping(conversation[currentDialogueIndex], texture);
    ...
}

DialogueBox.java

  • Kui tegelase portree (faceTexture) on olemas, joonistatakse see ekraanile dialoogiakna sisse.

public void render(SpriteBatch batch) {
    ...
    if (faceTexture != null) {
        batch.draw(faceTexture, X + FACE_PADDING, Y, FACE_SIZE, FACE_SIZE);
    }
    ...
}

Näide dialoogist, mis sisaldab tegelase portreed:

dialogue-with-face-gif

Lähtekood

  • Vaata näidisprojekti GitHubis: GitHub