Level 4 - Mesh-Mobs, Animationen und einfache KI
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.
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.
.gltf exportierenon_step bauenDas 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.
Lege im Luanti-Modordner einen neuen Mod mit exakt dieser Struktur an. Die Dateinamen sind fuer den ersten Test absichtlich festgelegt.
mods/
`-- kindermob/
|-- mod.conf
|-- init.lua
|-- models/
| `-- kindermob.gltf
`-- textures/
|-- kindermob.png
`-- kindermob_spawner.png
mod.confname = kindermob
title = Kindermob Unterrichtsmod
description = Ein einfacher animierter Mob aus Blockbench fuer Luanti.
author = diDeDi
Zuerst alle Namen exakt uebernehmen. Wenn der Mob funktioniert, duerfen Modell, Textur, Entity-Name und Chatbefehl angepasst werden. Das spart viel Fehlersuche.
models/kindermob.gltf speichern.textures/kindermob.png speichern.Wenn der Mob unsichtbar ist, liegt der Fehler sehr oft am Dateinamen, am falschen Ordner oder an eingebetteten Texturen.
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 |
_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.
init.luaKopiere den Code zuerst unveraendert. Teste danach einzelne Werte: Geschwindigkeit, Lebenspunkte, Schaden, Sichtweite, Name und Animationsbereiche.
-- ============================================================
-- 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
})
| 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. |
nametag = "Mein Drache"nametag_color = "#00FFAA"visual_size = {x = 0.7, y = 0.7, z = 0.7}_walk_speed = 4.0_view_range = 20_damage = 1if 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
-- Statt:
local direction = vector.direction(pos, target_pos)
-- Benutze:
local direction = vector.direction(target_pos, pos)
| 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. |
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.
1. Was ist das Gehirn des Mobs?
2. Wo liegt das 3D-Modell?
3. Warum speichern wir _current_animation?
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. |
/spawnkindermob funktioniert.