Level 4 - Mesh-Mobs, Animationen und einfache KI

Eigene Mobs in Luanti programmieren

In diesem Kapitel baust du aus einem Blockbench-Modell einen lebendigen Mob in Luanti: mit 3D-Modell, Textur, Animationen, Hitbox, Bewegung, Spieler-Erkennung, Angriff und Spawn-Item.

Lernziel

Am Ende hast du ein vollstaendiges Mob-Mod. Der Mob kann in der Welt gespawnt werden, sucht den naechsten Spieler, laeuft auf ihn zu, greift im Nahbereich an und spielt passende Animationen ab.

1. Was bauen wir?

Blockbench

  • 3D-Modell erstellen oder oeffnen
  • Textur pruefen
  • Animationen vorbereiten
  • Als .gltf exportieren

Lua in Luanti

  • Entity registrieren
  • Hitbox und Auswahlbox festlegen
  • Animationen starten
  • einfache KI in on_step bauen
Merksatz

Das Aussehen kommt aus Blockbench. Das Verhalten kommt aus Lua. Eine Entity ist ein bewegliches Objekt. Ein Mob ist eine Entity mit Verhalten, Lebenspunkten und Reaktionen auf Spieler.

2. Mod-Struktur

Lege im Luanti-Modordner einen neuen Mod mit exakt dieser Struktur an. Die Dateinamen sind fuer den ersten Test absichtlich festgelegt.

Ordnerstruktur
mods/
`-- kindermob/
    |-- mod.conf
    |-- init.lua
    |-- models/
    |   `-- kindermob.gltf
    `-- textures/
        |-- kindermob.png
        `-- kindermob_spawner.png

Datei mod.conf

kindermob/mod.conf
name = kindermob
title = Kindermob Unterrichtsmod
description = Ein einfacher animierter Mob aus Blockbench fuer Luanti.
author = diDeDi
Unterrichtsregel

Zuerst alle Namen exakt uebernehmen. Wenn der Mob funktioniert, duerfen Modell, Textur, Entity-Name und Chatbefehl angepasst werden. Das spart viel Fehlersuche.

3. Blockbench richtig exportieren

Wenn der Mob unsichtbar ist, liegt der Fehler sehr oft am Dateinamen, am falschen Ordner oder an eingebetteten Texturen.

4. Animationen verstehen

Der Code verwendet vier Zustände. Die Werte sind Beispielbereiche. Wenn deine Animationen in Blockbench andere Zeitbereiche haben, musst du nur die Tabelle ANIMATIONS anpassen.

Zustand Bedeutung Beispielbereich Wiederholen?
stand Mob steht ruhig. 0.0 bis 1.0 Ja
walk Mob laeuft. 1.0 bis 2.0 Ja
attack Mob greift an. 2.0 bis 2.6 Nein
die Mob stirbt. 2.6 bis 3.4 Nein
Warum _current_animation?

Wenn dieselbe Animation in jedem on_step-Durchlauf neu gestartet wird, sieht man kaum Bewegung. Deshalb merkt sich der Mob, welche Animation bereits laeuft.

5. Kompletter Code: init.lua

Kopiere den Code zuerst unveraendert. Teste danach einzelne Werte: Geschwindigkeit, Lebenspunkte, Schaden, Sichtweite, Name und Animationsbereiche.

kindermob/init.lua
-- ============================================================
-- Kindermob-Unterrichtsmod fuer Luanti
--
-- Der Mob kann:
--   - stehen
--   - laufen
--   - den naechsten Spieler suchen
--   - den Spieler verfolgen
--   - angreifen
--   - sterben
-- ============================================================

local modname = core.get_current_modname()

-- Vollstaendiger Entity-Name: "modname:name"
local MOB_NAME = modname .. ":schueler_mob"

-- Name des Spawn-Items
local SPAWNER_NAME = modname .. ":mob_spawner"


-- ============================================================
-- 1. Animationen
-- ============================================================
-- Diese Werte muessen zu euren Blockbench-Animationen passen.
-- Wenn die Animationen falsch aussehen, aendert zuerst diese Tabelle.
-- ============================================================

local ANIMATIONS = {
    stand = {
        range = {x = 0.0, y = 1.0},
        speed = 1,
        blend = 0.2,
        loop = true
    },

    walk = {
        range = {x = 1.0, y = 2.0},
        speed = 1,
        blend = 0.2,
        loop = true
    },

    attack = {
        range = {x = 2.0, y = 2.6},
        speed = 1,
        blend = 0.1,
        loop = false
    },

    die = {
        range = {x = 2.6, y = 3.4},
        speed = 1,
        blend = 0.1,
        loop = false
    }
}


-- ============================================================
-- 2. Hilfsfunktion: Animation starten
-- ============================================================
-- Diese Funktion verhindert, dass dieselbe Animation staendig
-- neu gestartet wird.
-- ============================================================

local function play_animation(self, animation_name, force_restart)
    local anim = ANIMATIONS[animation_name]

    if not anim then
        return
    end

    if self._current_animation == animation_name and not force_restart then
        return
    end

    self.object:set_animation(
        anim.range,
        anim.speed,
        anim.blend,
        anim.loop
    )

    self._current_animation = animation_name
end


-- ============================================================
-- 3. Hilfsfunktion: naechsten Spieler finden
-- ============================================================
-- Der Mob sucht den naechsten Spieler in einem bestimmten Radius.
-- ============================================================

local function get_nearest_player(pos, radius)
    local nearest_player = nil
    local nearest_distance = nil

    for _, player in ipairs(core.get_connected_players()) do
        local player_pos = player:get_pos()

        if player_pos then
            local distance = vector.distance(pos, player_pos)

            if distance <= radius then
                if nearest_distance == nil or distance < nearest_distance then
                    nearest_player = player
                    nearest_distance = distance
                end
            end
        end
    end

    return nearest_player, nearest_distance
end


-- ============================================================
-- 4. Hilfsfunktion: horizontale Geschwindigkeit setzen
-- ============================================================
-- Wir aendern nur X und Z. Y bleibt fuer Fallen und Schwerkraft erhalten.
-- ============================================================

local function set_horizontal_velocity(object, direction, speed)
    local old_velocity = object:get_velocity()

    object:set_velocity({
        x = direction.x * speed,
        y = old_velocity.y,
        z = direction.z * speed
    })
end


-- ============================================================
-- 5. Mob als Entity registrieren
-- ============================================================

core.register_entity(MOB_NAME, {

    initial_properties = {
        physical = true,
        collide_with_objects = true,
        hp_max = 20,
        pointable = true,

        -- Unsichtbare Kollisionsbox des Mobs.
        -- Format: {xmin, ymin, zmin, xmax, ymax, zmax}
        collisionbox = {-0.35, 0.0, -0.35, 0.35, 1.20, 0.35},
        selectionbox = {-0.40, 0.0, -0.40, 0.40, 1.25, 0.40},

        -- 3D-Modell verwenden.
        visual = "mesh",

        -- Datei muss im Ordner models liegen.
        mesh = "kindermob.gltf",

        -- Datei muss im Ordner textures liegen.
        textures = {"kindermob.png"},

        -- Groesse im Spiel.
        visual_size = {x = 1, y = 1, z = 1},

        -- Mob wird gespeichert, wenn der Weltbereich entladen wird.
        static_save = true,

        nametag = "Schueler-Mob",
        nametag_color = "#FFFF00",
    },

    -- Eigene Variablen unserer kleinen Mob-KI.
    _view_range = 12,
    _attack_range = 1.5,
    _walk_speed = 2.0,
    _damage = 2,
    _attack_cooldown = 0,
    _think_timer = 0,
    _dead = false,
    _current_animation = nil,


    -- Wird aufgerufen, wenn der Mob entsteht oder geladen wird.
    on_activate = function(self, staticdata, dtime_s)
        -- Schwerkraft aktivieren.
        self.object:set_acceleration({x = 0, y = -10, z = 0})

        -- Normale Verletzbarkeit.
        self.object:set_armor_groups({fleshy = 100})

        -- Gespeicherte Lebenspunkte laden, falls vorhanden.
        if staticdata and staticdata ~= "" then
            local data = core.deserialize(staticdata)

            if type(data) == "table" and data.hp then
                self.object:set_hp(data.hp)
            end
        end

        play_animation(self, "stand")
    end,


    -- Speichert wichtige Daten des Mobs.
    get_staticdata = function(self)
        return core.serialize({
            hp = self.object:get_hp()
        })
    end,


    -- Das Gehirn des Mobs.
    on_step = function(self, dtime, moveresult)
        if self._dead then
            return
        end

        -- Angriffscooldown herunterzaehlen.
        if self._attack_cooldown > 0 then
            self._attack_cooldown = self._attack_cooldown - dtime
        end

        -- Der Mob denkt nur alle 0.2 Sekunden.
        self._think_timer = self._think_timer + dtime

        if self._think_timer < 0.2 then
            return
        end

        self._think_timer = 0

        local pos = self.object:get_pos()
        if not pos then
            return
        end

        local target, distance = get_nearest_player(pos, self._view_range)

        -- Kein Spieler in der Naehe: stehen bleiben.
        if not target then
            local velocity = self.object:get_velocity()

            self.object:set_velocity({
                x = 0,
                y = velocity.y,
                z = 0
            })

            play_animation(self, "stand")
            return
        end

        local target_pos = target:get_pos()
        if not target_pos then
            return
        end

        -- Richtung zum Spieler berechnen.
        local direction = vector.direction(pos, target_pos)
        direction.y = 0

        if vector.length(direction) == 0 then
            return
        end

        direction = vector.normalize(direction)

        -- Mob zum Spieler drehen.
        self.object:set_yaw(core.dir_to_yaw(direction))

        -- Nah genug: angreifen.
        if distance <= self._attack_range then
            local velocity = self.object:get_velocity()

            self.object:set_velocity({
                x = 0,
                y = velocity.y,
                z = 0
            })

            if self._attack_cooldown <= 0 then
                play_animation(self, "attack", true)

                target:punch(
                    self.object,
                    1.0,
                    {
                        full_punch_interval = 1.0,
                        damage_groups = {fleshy = self._damage}
                    },
                    direction
                )

                self._attack_cooldown = 1.2
            end

            return
        end

        -- Sonst: Spieler verfolgen.
        set_horizontal_velocity(self.object, direction, self._walk_speed)
        play_animation(self, "walk")
    end,


    -- Wird aufgerufen, wenn jemand den Mob schlaegt.
    on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
        if self._dead then
            return
        end

        -- Kleiner Rueckstoss.
        if dir then
            self.object:add_velocity({
                x = dir.x * 1.5,
                y = 2,
                z = dir.z * 1.5
            })
        end
    end,


    -- Wird aufgerufen, wenn der Mob stirbt.
    on_death = function(self, killer)
        self._dead = true

        self.object:set_velocity({x = 0, y = 0, z = 0})
        self.object:set_acceleration({x = 0, y = 0, z = 0})

        play_animation(self, "die", true)

        local obj = self.object

        core.after(1.5, function()
            if obj and obj:is_valid() then
                obj:remove()
            end
        end)
    end,
})


-- ============================================================
-- 6. Chatbefehl zum Testen
-- Im Spiel: /spawnkindermob
-- ============================================================

core.register_chatcommand("spawnkindermob", {
    description = "Spawnt einen Schueler-Mob vor dir.",
    privs = {give = true},

    func = function(player_name)
        local player = core.get_player_by_name(player_name)

        if not player then
            return false, "Spieler nicht gefunden."
        end

        local pos = player:get_pos()
        local look_dir = player:get_look_dir()

        local spawn_pos = vector.add(pos, vector.multiply(look_dir, 3))
        spawn_pos.y = spawn_pos.y + 1

        local obj = core.add_entity(spawn_pos, MOB_NAME)

        if obj then
            return true, "Mob wurde gespawnt."
        else
            return false, "Mob konnte nicht gespawnt werden."
        end
    end
})


-- ============================================================
-- 7. Spawn-Item
-- Im Spiel: /giveme kindermob:mob_spawner
-- ============================================================

core.register_craftitem(SPAWNER_NAME, {
    description = "Schueler-Mob Spawner",
    inventory_image = "kindermob_spawner.png",

    on_place = function(itemstack, placer, pointed_thing)
        local spawn_pos = nil

        if pointed_thing and pointed_thing.type == "node" then
            spawn_pos = pointed_thing.above
        elseif placer then
            spawn_pos = placer:get_pos()
            spawn_pos.y = spawn_pos.y + 1
        end

        if spawn_pos then
            core.add_entity(spawn_pos, MOB_NAME)
        end

        -- Fuer den Unterricht verbrauchen wir den Spawner nicht.
        return itemstack
    end
})

6. Was passiert im Code?

Teil Aufgabe Warum wichtig?
initial_properties Beschreibt Modell, Textur, Hitbox, Lebenspunkte und Sichtbarkeit. Ohne diese Werte weiss Luanti nicht, wie der Mob aussehen und kollidieren soll.
on_activate Startet Schwerkraft, Ruestung und Stand-Animation. Der Mob wird beim Spawn oder Laden korrekt vorbereitet.
on_step Sucht Spieler, dreht den Mob, bewegt ihn und startet Angriffe. Das ist das Gehirn des Mobs.
on_punch Reagiert auf Schlaege mit Rueckstoss. Der Mob fuehlt sich im Spiel koerperlicher an.
on_death Stoppt den Mob, spielt Sterbe-Animation und entfernt ihn. Der Tod ist sichtbar und endet sauber.
register_craftitem Registriert ein Spawn-Item. Der Mob kann ohne Chatbefehl in der Welt platziert werden.

7. Aufgaben

Level 1: sichtbar aendern

  • Nametag aendern: nametag = "Mein Drache"
  • Farbe aendern: nametag_color = "#00FFAA"
  • Groesse aendern: visual_size = {x = 0.7, y = 0.7, z = 0.7}

Level 2: Verhalten aendern

  • Tempo erhoehen: _walk_speed = 4.0
  • Sichtweite erhoehen: _view_range = 20
  • Schaden senken: _damage = 1

Level 3: Mob-Typ bauen

  • Boss: viel Leben, langsam, hoher Schaden
  • Haustier: folgt, greift aber nicht an
  • Feigling: laeuft vor Spielern weg

Freundlicher Mob: Angriffsteil ersetzen

Kein Angriff, nur stehen bleiben
if distance <= self._attack_range then
    local velocity = self.object:get_velocity()

    self.object:set_velocity({
        x = 0,
        y = velocity.y,
        z = 0
    })

    play_animation(self, "stand")
    return
end

Feigling-Mob: Richtung umdrehen

Vor dem Spieler weglaufen
-- Statt:
local direction = vector.direction(pos, target_pos)

-- Benutze:
local direction = vector.direction(target_pos, pos)

8. Fehlersuche

Problem Wahrscheinliche Ursache Sofortloesung
Mob ist unsichtbar. Modellname oder Ordner falsch. mesh = "kindermob.gltf" und Ordner models/ pruefen.
Textur fehlt. Textur falsch benannt oder eingebettet. Textur separat in textures/ speichern.
Mob ist zu gross oder zu klein. Export-Skalierung oder visual_size. visual_size und Blockbench-Skalierung pruefen.
Animation laeuft nicht. Falscher Animationsbereich. Werte in ANIMATIONS anpassen.
Mob schwebt. Keine Schwerkraft oder falsche Kollisionsbox. set_acceleration({x = 0, y = -10, z = 0}) und collisionbox pruefen.
Mob faellt durch den Boden. physical oder Kollisionsbox falsch. physical = true und collisionbox pruefen.
Welt ruckelt. Zu viele Mobs oder zu viel Logik pro Frame. Timer in on_step verwenden und Mob-Anzahl begrenzen.
Lehrer-Trick

Wenn das Modell Probleme macht, teste den Lua-Code zuerst mit einem einfachen bekannten Mesh oder mit dem bereits funktionierenden Beispiel-Mob. So trennst du Codefehler von Exportfehlern.

9. Mini-Quiz

1. Was ist das Gehirn des Mobs?

2. Wo liegt das 3D-Modell?

3. Warum speichern wir _current_animation?

10. Abschlussprojekt

Jede Gruppe baut einen eigenen Mob-Typ und praesentiert danach, welche Werte und Code-Stellen geaendert wurden.

Mob-Typ Verhalten Was muss geaendert werden?
Waechter Verfolgt und greift an. Sichtweite, Schaden, Name.
Haustier Folgt, aber greift nicht an. Angriffsteil ersetzen.
Feigling Laeuft vor Spielern weg. Richtung umdrehen.
Boss Langsam, stark, viele Lebenspunkte. hp_max, _damage, _walk_speed.
Mini-Monster Klein und schnell. visual_size, collisionbox, Geschwindigkeit.

Fertig-Checkliste