MoonScript - Компилятор для Igdrasil Engine
MoonScript — это язык программирования, который компилируется в Lua. Он предоставляет более выразительный синтаксис с поддержкой классов, списковых включений, деструктуризации и многого другого.
Основные возможности MoonScript
✅ Компиляция в чистый Lua код
✅ Объектно-ориентированное программирование с классами
✅ Списковые включения (list comprehensions)
✅ Деструктуризация переменных
✅ Условные выражения и switch/case
✅ Упрощённый синтаксис функций
✅ Автоматическое определение локальных переменных
Компилятор MoonScript
Используйте класс MoonScriptCompiler для компиляции MoonScript кода в Lua:
using IgdrasilEngine.Engine.Scripting.Lua;
// Компиляция MoonScript в Lua
string moonCode = @"
class Animal
new: (@name) =>
speak: =>
print 'Hello, my name is ' .. @name
dog = Animal 'Buddy'
dog\speak!
";
string luaCode = MoonScriptCompiler.Compile(moonCode);
// Теперь можно выполнить скомпилированный Lua код
var engine = new LuaEngine();
engine.Execute(luaCode);
Синтаксис MoonScript
Классы
class Player
new: (@name, @health = 100) =>
attack: (target) =>
print @name .. " attacks " .. target
heal: (amount) =>
@health += amount
print @name .. " healed for " .. amount
-- Использование
player = Player "Hero"
player\attack "Dragon"
player\heal 50
Скомпилированный Lua код:
local Player
do
local _class_0
local _base_0 = {
attack = function(self, target)
return print(self.name .. " attacks " .. target)
end,
heal = function(self, amount)
self.health = self.health + amount
return print(self.name .. " healed for " .. tostring(amount))
end
}
_base_0.__index = _base_0
_class_0 = setmetatable({
__init = function(self, name, health)
self.name = name
if health == nil then
health = 100
end
self.health = health
end
}, {
__index = _base_0,
__call = function(cls, ...)
local _self_0 = setmetatable({}, _base_0)
cls.__init(_self_0, ...)
return _self_0
end
})
Player = _class_0
end
Наследование
class Animal
new: (@name) =>
speak: =>
print @name .. " makes a sound"
class Dog extends Animal
speak: =>
print @name .. " barks!"
class Cat extends Animal
speak: =>
print @name .. " meows!"
dog = Dog "Buddy"
cat = Cat "Whiskers"
dog\speak! -- "Buddy barks!"
cat\speak! -- "Whiskers meows!"
Списковые включения
-- Простое включение
squares = [x * x for x in *{1, 2, 3, 4, 5}]
-- результат: {1, 4, 9, 16, 25}
-- С условием
evens = [x for x in *{1, 2, 3, 4, 5, 6} when x % 2 == 0]
-- результат: {2, 4, 6}
-- Вложенные включения
pairs = [{x, y} for x in *{1, 2, 3} for y in *{4, 5, 6}]
-- результат: {{1,4}, {1,5}, {1,6}, {2,4}, ...}
-- Включения для таблиц
user_map = {user.id, user.name for user in *users}
-- результат: {[1]="Alice", [2]="Bob", ...}
Условные выражения
-- Тернарный оператор
status = if health > 0 then "alive" else "dead"
-- Многострочные условия
message = if score > 90
"Отлично!"
elseif score > 70
"Хорошо!"
elseif score > 50
"Удовлетворительно"
else
"Плохо"
-- Switch выражение
result = switch value
when 1 then "one"
when 2 then "two"
when 3 then "three"
else "other"
-- Unless (противоположность if)
print "Game Over" unless player.alive
-- Постфиксные условия
save_game! if auto_save_enabled
reload_level! unless level_completed
Деструктуризация
-- Таблицы
{x, y, z} = getPosition!
-- С остальными элементами
{first, second, ...rest} = {1, 2, 3, 4, 5}
-- first=1, second=2, rest={3, 4, 5}
-- Функции возвращающие несколько значений
{status, message} = validateInput data
-- С значениями по умолчанию
{name = "Unknown", age = 0} = getUserData!
-- Вложенная деструктуризация
{player: {x, y}, score} = getGameState!
Функции
-- Стрелочные функции
double = (x) -> x * 2
add = (a, b) -> a + b
-- Функции с жирной стрелкой (сохраняют self)
class Button
onClick: (callback) =>
@callback = callback
trigger: =>
@callback! if @callback
-- Значения по умолчанию
greet = (name = "Guest") -> print "Hello, " .. name
-- Variadic функции
sum = (...) ->
total = 0
for v in *{...}
total += v
total
-- Именованные параметры через таблицу
createWindow = ({width = 800, height = 600, title = "Window"}) ->
print "Creating #{width}x#{height} window: #{title}"
Интерполяция строк
-- Двойные кавычки поддерживают интерполяцию
name = "World"
message = "Hello, #{name}!"
-- Можно вставлять выражения
x, y = 10, 20
coords = "Position: (#{x}, #{y})"
-- Вызов функций
player = {name: "Hero", level: 10}
info = "#{player.name} is level #{player.level}"
Операторы
-- Составные операторы
x += 5 -- x = x + 5
y -= 3 -- y = y - 3
z *= 2 -- z = z * 2
w /= 4 -- w = w / 4
s ..= "!" -- s = s .. "!"
-- Логические операторы
result = true and false -- false
result = true or false -- true
result = not true -- false
-- С присваиванием
x = x or 10 -- можно написать как:
x or= 10
-- Проверка на nil
value = getValue!
value or= "default"
Циклы
-- For по числам
for i = 1, 10
print i
-- For по таблице (массив)
for item in *items
print item
-- For по таблице (ключ-значение)
for key, value in pairs table
print key, value
-- While
while player.alive
update!
-- Until (противоположность while)
until game_over
play_frame!
-- Постфиксные циклы
print item for item in *items
With
-- Упрощает доступ к полям объекта
with player
.x = 100
.y = 200
.health = 100
\update!
-- Эквивалентно:
player.x = 100
player.y = 200
player.health = 100
player:update()
-- С возвращаемым значением
result = with create_object!
.name = "MyObject"
.value = 42
Интеграция в движок
Методы расширения
public static class LuaEngineExtensions
{
/// <summary>
/// Выполняет MoonScript код, предварительно скомпилировав его в Lua.
/// </summary>
public static object[] ExecuteMoonScript(this LuaEngine engine, string moonCode)
{
string luaCode = MoonScriptCompiler.Compile(moonCode);
return engine.Execute(luaCode);
}
/// <summary>
/// Компилирует MoonScript в Lua и создаёт скомпилированный скрипт.
/// </summary>
public static LuaScript CompileMoonScript(this LuaEngine engine, string moonCode)
{
string luaCode = MoonScriptCompiler.Compile(moonCode);
return engine.Compile(luaCode);
}
/// <summary>
/// Оценивает MoonScript выражение и возвращает результат.
/// </summary>
public static object[] EvaluateMoonScript(this LuaEngine engine, string moonCode)
{
string luaCode = MoonScriptCompiler.Compile(moonCode);
return engine.Evaluate(luaCode);
}
}
Использование
var engine = new LuaEngine();
// Прямое выполнение
var result = engine.ExecuteMoonScript(@"
add = (a, b) -> a + b
return add 5, 3
");
Console.WriteLine(result[0]); // 8
// Компиляция для повторного использования
var script = engine.CompileMoonScript(@"
multiply = (a, b) -> a * b
return multiply x, y
");
script.SetValue("x", 10);
script.SetValue("y", 5);
var result = script.Evaluate();
Console.WriteLine(result[0]); // 50
Загрузка из файлов
// Загрузка MoonScript из файла
string moonCode = File.ReadAllText("script.moon");
string luaCode = MoonScriptCompiler.Compile(moonCode);
var engine = new LuaEngine();
engine.Execute(luaCode);
// Кеширование скомпилированного кода
var cache = new Dictionary<string, string>();
string GetCompiledScript(string path)
{
if (!cache.TryGetValue(path, out string luaCode))
{
string moonCode = File.ReadAllText(path);
luaCode = MoonScriptCompiler.Compile(moonCode);
cache[path] = luaCode;
}
return luaCode;
}
Обработка ошибок
Ошибки компиляции
using IgdrasilEngine.Engine.Scripting.Exceptions;
try
{
string moonCode = @"
class Invalid
bad syntax here
";
string luaCode = MoonScriptCompiler.Compile(moonCode);
}
catch (NLua.Exceptions.LuaException ex)
{
Console.WriteLine($"Ошибка компиляции MoonScript: {ex.Message}");
// Пример вывода:
// Parse error: [string "compiler"]:4: unexpected symbol near 'bad'
}
Ошибки выполнения
try
{
var engine = new LuaEngine();
string luaCode = MoonScriptCompiler.Compile(@"
divide = (a, b) -> a / b
return divide 10, 0
");
var result = engine.Execute(luaCode);
}
catch (ScriptExecutionException ex)
{
Console.WriteLine($"Ошибка выполнения: {ex.Message}");
}
Отладка
Поскольку MoonScript компилируется в Lua, ошибки времени выполнения будут ссылаться на строки Lua кода:
// Сохраняем оба варианта для отладки
string moonCode = File.ReadAllText("game.moon");
string luaCode = MoonScriptCompiler.Compile(moonCode);
// Сохраняем скомпилированный код для отладки
File.WriteAllText("game.lua", luaCode);
try
{
engine.Execute(luaCode);
}
catch (Exception ex)
{
Console.WriteLine($"Ошибка в game.lua: {ex.Message}");
Console.WriteLine("Проверьте game.lua для отладки");
}
Практические примеры
Игровая логика
-- Система врагов
class Enemy
new: (@name, @health, @damage) =>
@alive = true
takeDamage: (amount) =>
@health -= amount
if @health <= 0
@onDeath!
onDeath: =>
@alive = false
print "#{@name} has been defeated!"
attack: (target) =>
if @alive
target\takeDamage @damage
print "#{@name} attacks for #{@damage} damage!"
-- Создание волны врагов
spawnWave = (count, prefix) ->
[Enemy "#{prefix} #{i}", 50 + i * 10, 10 for i in *[1..count]]
-- Использование
enemies = spawnWave 5, "Goblin"
for enemy in *enemies when enemy.alive
enemy\attack player
Система инвентаря
class Item
new: (@name, @value, @weight) =>
class Inventory
new: (@capacity = 20) =>
@items = {}
@weight = 0
canAdd: (item) =>
@weight + item.weight <= @capacity
add: (item) =>
if @canAdd item
table.insert @items, item
@weight += item.weight
true
else
false
remove: (item) =>
for i, inv_item in ipairs @items
if inv_item == item
table.remove @items, i
@weight -= item.weight
return true
false
getTotalValue: =>
sum = 0
for item in *@items
sum += item.value
sum
-- Использование
inventory = Inventory 50
inventory\add Item "Sword", 100, 5
inventory\add Item "Shield", 80, 10
print "Total value: #{inventory\getTotalValue!}"
Конечный автомат (State Machine)
class StateMachine
new: (@initial_state) =>
@current_state = @initial_state
@states = {}
addState: (name, state) =>
@states[name] = state
transition: (new_state_name) =>
new_state = @states[new_state_name]
return unless new_state
@current_state\exit! if @current_state.exit
@current_state = new_state
@current_state\enter! if @current_state.enter
update: (dt) =>
@current_state\update dt if @current_state.update
-- Состояния игрока
idle_state = {
enter: -> print "Entering idle"
update: (dt) -> -- обновление
exit: -> print "Exiting idle"
}
walk_state = {
enter: -> print "Start walking"
update: (dt) -> -- обновление ходьбы
exit: -> print "Stop walking"
}
-- Использование
fsm = StateMachine idle_state
fsm\addState "idle", idle_state
fsm\addState "walk", walk_state
fsm\transition "walk"
Обработка данных
-- Парсинг и фильтрация конфигурации
parseConfig = (raw_data) ->
lines = [line for line in raw_data\gmatch "[^\n]+"]
config = {}
for line in *lines
continue if line\match "^%s*#" -- пропуск комментариев
continue if line\match "^%s*$" -- пропуск пустых строк
key, value = line\match "(%S+)%s*=%s*(.+)"
if key and value
config[key] = value
config
-- Агрегация статистики
calculateStats = (players) ->
total_score = 0
max_score = 0
min_score = math.huge
for player in *players
total_score += player.score
max_score = math.max max_score, player.score
min_score = math.min min_score, player.score
{
average: total_score / #players
max: max_score
min: min_score
total: total_score
}
Преимущества MoonScript
1. Читаемость
Lua:
local function calculate(x, y, z)
local result = x * y
if result > z then
return result - z
else
return result + z
end
end
MoonScript:
calculate = (x, y, z) ->
result = x * y
if result > z then result - z else result + z
2. ООП
Lua (требует метатаблицы):
local Player = {}
Player.__index = Player
function Player.new(name, health)
local self = setmetatable({}, Player)
self.name = name
self.health = health
return self
end
function Player:heal(amount)
self.health = self.health + amount
end
MoonScript:
class Player
new: (@name, @health) =>
heal: (amount) =>
@health += amount
3. Списковые включения
Lua:
local squares = {}
for i = 1, 10 do
if i % 2 == 0 then
table.insert(squares, i * i)
end
end
MoonScript:
squares = [i * i for i in *[1..10] when i % 2 == 0]
4. Безопасность
MoonScript автоматически делает переменные локальными, предотвращая случайное загрязнение глобальной области видимости:
Lua (ошибка - глобальная переменная):
function calculate()
result = 42 -- глобальная!
return result
end
MoonScript (автоматически локальная):
calculate = ->
result = 42 -- локальная
result
Ограничения
1. Время компиляции
Компиляция MoonScript добавляет накладные расходы:
var sw = Stopwatch.StartNew();
string luaCode = MoonScriptCompiler.Compile(moonCode);
sw.Stop();
Console.WriteLine($"Compilation took: {sw.ElapsedMilliseconds}ms");
Рекомендация: Компилируйте заранее для продакшн сборок.
2. Отладка
Ошибки ссылаются на строки Lua кода:
-- script.moon, строка 5
class Player
attack: =>
@health += 10 -- ошибка: должно быть -=
Ошибка покажет строку в Lua коде, не в MoonScript.
Решение: Сохраняйте скомпилированный Lua код для отладки.
3. Размер сборки
Компилятор MoonScript встроен в сборку, увеличивая её размер (~200KB).
4. Совместимость
Не весь Lua код можно написать в MoonScript идиоматично. Некоторые низкоуровневые операции проще писать на Lua.
Рекомендации
1. Используйте для сложной логики
MoonScript отлично подходит для:
- Игровой логики
- ИИ поведения
- Систем управления состояниями
- Обработки данных
2. Кешируйте результаты
public class MoonScriptCache
{
private readonly Dictionary<string, string> _cache = new();
public string GetCompiled(string moonCode)
{
string hash = ComputeHash(moonCode);
if (!_cache.TryGetValue(hash, out string luaCode))
{
luaCode = MoonScriptCompiler.Compile(moonCode);
_cache[hash] = luaCode;
}
return luaCode;
}
private string ComputeHash(string input)
{
using var sha = SHA256.Create();
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(input));
return Convert.ToBase64String(bytes);
}
}
3. Разделяйте разработку и продакшн
Development:
// Компиляция на лету для быстрой итерации
string moonCode = File.ReadAllText("script.moon");
string luaCode = MoonScriptCompiler.Compile(moonCode);
engine.Execute(luaCode);
Production:
// Предварительная компиляция
// Build script:
// moonc -t lua_scripts script.moon
string luaCode = File.ReadAllText("script.lua");
engine.Execute(luaCode);
4. Документируйте
Используйте комментарии для облегчения отладки:
-- Система здоровья игрока
-- @param amount количество здоровья для восстановления
-- @return новое значение здоровья
heal = (amount) ->
@health = math.min @health + amount, @max_health
print "Healed for #{amount}, current health: #{@health}"
@health
5. Тестируйте
[Test]
public void TestMoonScriptCompilation()
{
string moonCode = @"
add = (a, b) -> a + b
return add 5, 3
";
string luaCode = MoonScriptCompiler.Compile(moonCode);
Assert.IsNotNull(luaCode);
var engine = new LuaEngine();
var result = engine.Execute(luaCode);
Assert.AreEqual(8.0, result[0]);
}
Производительность
Сравнение времени компиляции
| Размер скрипта | Время компиляции |
|---|---|
| Маленький (< 100 строк) | < 10ms |
| Средний (100-500 строк) | 10-50ms |
| Большой (500-1000 строк) | 50-150ms |
| Очень большой (> 1000 строк) | > 150ms |
Оптимизация
// Плохо: компиляция в каждом кадре
void Update()
{
string luaCode = MoonScriptCompiler.Compile(moonScript);
engine.Execute(luaCode);
}
// Хорошо: компиляция один раз
private LuaScript _compiledScript;
void Start()
{
string luaCode = MoonScriptCompiler.Compile(moonScript);
_compiledScript = engine.Compile(luaCode);
}
void Update()
{
_compiledScript.Evaluate();
}
Ресурсы
Официальная документация
- MoonScript Reference - Полная справка по синтаксису
- MoonScript Compiler - Онлайн компилятор для тестирования
- MoonScript GitHub - Исходный код
Примеры
- MoonScript Examples - Примеры кода
- Learn MoonScript in Y Minutes - Быстрое введение
Сообщество
- Lua Users Wiki - Ресурсы по Lua