feat: 50 игр на Lua + импорт Roblox для всех + поддержка Lua в плеере #39
@ -2616,10 +2616,12 @@ end`}</Code>}
|
||||
<>
|
||||
<p>
|
||||
<b>NPC</b> (неигровой персонаж) — это житель твоей игры:
|
||||
торговец, враг, проводник. Создаётся командой
|
||||
<code> game.scene.spawnNpc(модель, опции)</code>.
|
||||
торговец, враг, проводник.
|
||||
</p>
|
||||
<ScriptKind kind="global" />
|
||||
<LangTabs
|
||||
js={<>
|
||||
<p>В JS используем <code>game.scene.spawnNpc(модель, опции)</code>:</p>
|
||||
<Code>{`// Создаём NPC по имени Боб
|
||||
const bob = game.scene.spawnNpc('character-a', {
|
||||
x: 5, y: 0, z: 0,
|
||||
@ -2628,11 +2630,8 @@ const bob = game.scene.spawnNpc('character-a', {
|
||||
speed: 3
|
||||
});
|
||||
|
||||
// Боб говорит реплику над головой (3 секунды)
|
||||
bob.say('Привет, путник!', 3);
|
||||
|
||||
// Боб идёт в точку (x = 10, z = 0)
|
||||
bob.moveTo(10, 0);`}</Code>
|
||||
bob.say('Привет, путник!', 3); // реплика над головой
|
||||
bob.moveTo(10, 0); // идёт в точку`}</Code>
|
||||
<p><b>Что умеет NPC:</b></p>
|
||||
<table className="docTable">
|
||||
<tbody>
|
||||
@ -2641,8 +2640,8 @@ bob.moveTo(10, 0);`}</Code>
|
||||
<tr><td><code>stop()</code></td><td>остановиться</td></tr>
|
||||
<tr><td><code>say(текст, сек)</code></td><td>реплика над головой</td></tr>
|
||||
<tr><td><code>damage(n)</code></td><td>нанести урон NPC</td></tr>
|
||||
<tr><td><code>remove()</code></td><td>убрать со сцены</td></tr>
|
||||
<tr><td><code>onDeath(fn)</code></td><td>что сделать при гибели</td></tr>
|
||||
<tr><td><code>remove()</code></td><td>убрать</td></tr>
|
||||
<tr><td><code>onDeath(fn)</code></td><td>при гибели</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><b>Пример — враг гонится за игроком:</b></p>
|
||||
@ -2650,13 +2649,65 @@ bob.moveTo(10, 0);`}</Code>
|
||||
x: 0, y: 0, z: 20, name: 'Враг', hp: 50, speed: 2
|
||||
});
|
||||
|
||||
enemy.follow('player'); // началась погоня
|
||||
|
||||
enemy.follow('player');
|
||||
enemy.onDeath(() => {
|
||||
game.ui.showText('Враг побеждён!', 2);
|
||||
game.scene.spawnParticles('explosion',
|
||||
enemy.position, { duration: 1 });
|
||||
});`}</Code>
|
||||
</>}
|
||||
lua={<>
|
||||
<p>
|
||||
В Lua NPC — это обычный Model с Humanoid внутри.
|
||||
Движение делается через <code>humanoid:MoveTo(point)</code>.
|
||||
Реплики — через ChatService или BillboardGui.
|
||||
</p>
|
||||
<Code>{`-- NPC модель должна лежать в Workspace.
|
||||
-- Внутри Model должны быть Part'ы и Humanoid.
|
||||
local npc = workspace:WaitForChild("Боб")
|
||||
local humanoid = npc:WaitForChild("Humanoid")
|
||||
local hrp = npc:WaitForChild("HumanoidRootPart")
|
||||
|
||||
-- Реплика над головой через BillboardGui
|
||||
local function say(text, duration)
|
||||
local bg = Instance.new("BillboardGui")
|
||||
bg.Size = UDim2.new(4, 0, 1, 0)
|
||||
bg.StudsOffset = Vector3.new(0, 3, 0)
|
||||
bg.Parent = npc.Head or hrp
|
||||
local label = Instance.new("TextLabel", bg)
|
||||
label.Size = UDim2.new(1, 0, 1, 0)
|
||||
label.BackgroundTransparency = 1
|
||||
label.TextColor3 = Color3.new(1, 1, 1)
|
||||
label.TextScaled = true
|
||||
label.Text = text
|
||||
task.delay(duration, function() bg:Destroy() end)
|
||||
end
|
||||
|
||||
say("Привет, путник!", 3)
|
||||
|
||||
-- Идёт в точку
|
||||
humanoid:MoveTo(Vector3.new(10, hrp.Position.Y, 0))`}</Code>
|
||||
<p><b>Враг гонится за игроком:</b></p>
|
||||
<Code>{`local enemy = workspace:WaitForChild("Враг")
|
||||
local humanoid = enemy.Humanoid
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
-- Каждые 0.5 сек обновляем цель — позицию игрока
|
||||
task.spawn(function()
|
||||
while enemy.Parent do
|
||||
local player = Players:GetPlayers()[1]
|
||||
if player and player.Character then
|
||||
local target = player.Character.HumanoidRootPart.Position
|
||||
humanoid:MoveTo(target)
|
||||
end
|
||||
task.wait(0.5)
|
||||
end
|
||||
end)
|
||||
|
||||
-- При гибели
|
||||
humanoid.Died:Connect(function()
|
||||
print("Враг побеждён!")
|
||||
end)`}</Code>
|
||||
</>}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -2666,15 +2717,16 @@ enemy.onDeath(() => {
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
<b>Инвентарь</b> — это сумка предметов внизу экрана.
|
||||
<b> Инструмент</b> — предмет, который игрок берёт в руку:
|
||||
меч, фонарик, лопата.
|
||||
<b>Инвентарь</b> — сумка предметов. <b>Инструмент</b> —
|
||||
предмет, который игрок берёт в руку: меч, фонарик.
|
||||
</p>
|
||||
<ScriptKind kind="global" />
|
||||
<LangTabs
|
||||
js={<>
|
||||
<Code>{`// Выдать игроку меч прямо в руку
|
||||
game.player.giveTool('sword', {
|
||||
name: 'Стальной меч',
|
||||
equip: true // сразу взять в руку
|
||||
equip: true
|
||||
});
|
||||
|
||||
// Ловим, когда игрок применил инструмент (ЛКМ)
|
||||
@ -2682,23 +2734,65 @@ game.player.onToolUse((e) => {
|
||||
game.log('Игрок применил:', e.tool);
|
||||
});`}</Code>
|
||||
<p>
|
||||
Команды отдела <code>game.inventory</code>:
|
||||
<code> add(item)</code> — добавить предмет,
|
||||
<code> remove(имя)</code> — убрать,
|
||||
<code> has(имя)</code> — есть ли предмет,
|
||||
<code> list()</code> — список всех предметов.
|
||||
Команды <code>game.inventory</code>: <code>add</code>,
|
||||
<code> remove</code>, <code>has</code>, <code>list</code>.
|
||||
</p>
|
||||
</>}
|
||||
lua={<>
|
||||
<Code>{`-- В Roblox инструмент — это Tool-инстанс в Backpack игрока.
|
||||
local Players = game:GetService("Players")
|
||||
local player = Players.LocalPlayer
|
||||
|
||||
-- Создаём меч
|
||||
local sword = Instance.new("Tool")
|
||||
sword.Name = "Стальной меч"
|
||||
sword.RequiresHandle = false -- упрощённо без Handle-Part
|
||||
sword.Parent = player.Backpack
|
||||
|
||||
-- Сразу взять в руку (переложить в Character)
|
||||
sword.Parent = player.Character
|
||||
|
||||
-- Ловим применение (ЛКМ или активация)
|
||||
sword.Activated:Connect(function()
|
||||
print("Игрок применил меч!")
|
||||
end)`}</Code>
|
||||
<p>
|
||||
Инвентарь игрока = его <code>Backpack</code> (Roblox-сервис).
|
||||
Чтобы посмотреть что есть: <code>player.Backpack:GetChildren()</code>.
|
||||
</p>
|
||||
</>}
|
||||
/>
|
||||
<p><b>Пример — игра «ключ и сундук»:</b></p>
|
||||
<ScriptKind kind="object" on="сундук" />
|
||||
<Code>{`game.self.onInteract(() => {
|
||||
// проверяем, есть ли у игрока ключ
|
||||
<LangTabs
|
||||
js={<Code>{`game.self.onInteract(() => {
|
||||
if (game.inventory.has('Ключ')) {
|
||||
game.ui.showText('Сундук открыт!', 2);
|
||||
game.inventory.remove('Ключ'); // ключ потрачен
|
||||
game.inventory.remove('Ключ');
|
||||
} else {
|
||||
game.ui.showText('Нужен ключ', 1.5);
|
||||
}
|
||||
}, { text: 'Открыть', distance: 4 });`}</Code>
|
||||
}, { text: 'Открыть', distance: 4 });`}</Code>}
|
||||
lua={<Code>{`local part = script.Parent
|
||||
local prompt = Instance.new("ProximityPrompt")
|
||||
prompt.ActionText = "Открыть"
|
||||
prompt.MaxActivationDistance = 4
|
||||
prompt.Parent = part
|
||||
|
||||
prompt.Triggered:Connect(function(player)
|
||||
-- Ищем ключ в Backpack
|
||||
local key = player.Backpack:FindFirstChild("Ключ")
|
||||
if not key then
|
||||
key = player.Character and player.Character:FindFirstChild("Ключ")
|
||||
end
|
||||
if key then
|
||||
print("Сундук открыт!")
|
||||
key:Destroy() -- ключ потрачен
|
||||
else
|
||||
print("Нужен ключ")
|
||||
end
|
||||
end)`}</Code>}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -2707,16 +2801,16 @@ game.player.onToolUse((e) => {
|
||||
title: 'G3. Звук: свои звуки и 3D-позиционный звук',
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
Звук оживляет игру. Команда
|
||||
<code> game.sound.play(id, опции)</code>.
|
||||
</p>
|
||||
<p>Звук оживляет игру.</p>
|
||||
<ScriptKind kind="global" />
|
||||
<LangTabs
|
||||
js={<>
|
||||
<p>В JS — команда <code>game.sound.play(id, опции)</code>:</p>
|
||||
<Code>{`// Готовые звуки-пресеты
|
||||
game.sound.play('coin'); // звон монетки
|
||||
game.sound.play('win'); // победа
|
||||
game.sound.play('jump'); // прыжок
|
||||
game.sound.play('hit'); // удар
|
||||
game.sound.play('coin');
|
||||
game.sound.play('win');
|
||||
game.sound.play('jump');
|
||||
game.sound.play('hit');
|
||||
|
||||
// Свой загруженный звук, потише
|
||||
game.sound.play('sound_1', { volume: 0.7 });`}</Code>
|
||||
@ -2726,19 +2820,40 @@ game.sound.play('sound_1', { volume: 0.7 });`}</Code>
|
||||
<code> hit</code>, <code>coin</code>.
|
||||
</p>
|
||||
<p>
|
||||
<b>3D-звук</b> — если указать опцию <code>at</code>,
|
||||
звук пойдёт из точки в мире: чем дальше игрок, тем тише.
|
||||
<b>3D-звук</b> — опция <code>at</code> привязывает
|
||||
звук к точке в мире, тише с расстоянием.
|
||||
</p>
|
||||
<Code>{`// Звук костра — слышен только когда подходишь близко
|
||||
game.sound.play('sound_2', {
|
||||
<Code>{`game.sound.play('sound_2', {
|
||||
at: { x: 0, y: 1, z: 0 },
|
||||
loop: true // звук повторяется по кругу
|
||||
loop: true
|
||||
});`}</Code>
|
||||
</>}
|
||||
lua={<>
|
||||
<p>В Lua используется <code>Sound</code>-инстанс:</p>
|
||||
<Code>{`-- Простой звук — играет везде одинаково
|
||||
local sound = Instance.new("Sound")
|
||||
sound.SoundId = "rbxassetid://9120386436" -- свой ID
|
||||
sound.Volume = 0.7
|
||||
sound.Parent = workspace
|
||||
sound:Play()`}</Code>
|
||||
<p>
|
||||
<b>3D-звук</b> — родителем ставим Part в мире.
|
||||
Sound автоматически становится позиционным.
|
||||
</p>
|
||||
<Code>{`-- Звук костра — слышен близко
|
||||
local campfire = workspace.Костёр
|
||||
local sound = Instance.new("Sound")
|
||||
sound.SoundId = "rbxassetid://..."
|
||||
sound.RollOffMaxDistance = 30 -- метры до полной тишины
|
||||
sound.Looped = true
|
||||
sound.Parent = campfire -- родитель = Part → 3D-звук
|
||||
sound:Play()`}</Code>
|
||||
</>}
|
||||
/>
|
||||
<Note>
|
||||
Звук в играх обязателен — игра без звука кажется
|
||||
«мёртвой». Но не запускай длинную музыку в самом начале:
|
||||
это скучно и тормозит старт. Звуки вешай на события:
|
||||
прыжок, попадание, победа.
|
||||
Звук обязателен — игра без звука кажется «мёртвой».
|
||||
Но не запускай длинную музыку в начале — это тормозит старт.
|
||||
Звуки вешай на события: прыжок, попадание, победа.
|
||||
</Note>
|
||||
</>
|
||||
),
|
||||
@ -2748,29 +2863,65 @@ game.sound.play('sound_2', {
|
||||
title: 'G4. Камера: FOV, привязка, катсцены',
|
||||
body: (
|
||||
<>
|
||||
<p>Отдел <code>game.camera</code> управляет видом игрока:</p>
|
||||
<ScriptKind kind="global" />
|
||||
<LangTabs
|
||||
js={<>
|
||||
<p>В JS — отдел <code>game.camera</code>:</p>
|
||||
<table className="docTable">
|
||||
<tbody>
|
||||
<tr><td><code>setFov(градусы)</code></td><td>угол обзора — больше «шире» видно</td></tr>
|
||||
<tr><td><code>shake(сила, сек)</code></td><td>тряска камеры (взрыв, удар)</td></tr>
|
||||
<tr><td><code>focusOn(ref)</code></td><td>навести камеру на объект</td></tr>
|
||||
<tr><td><code>cutscene(точки, опции)</code></td><td>пролёт камеры по точкам</td></tr>
|
||||
<tr><td><code>reset()</code></td><td>вернуть камеру игроку</td></tr>
|
||||
<tr><td><code>setFov(градусы)</code></td><td>угол обзора</td></tr>
|
||||
<tr><td><code>shake(сила, сек)</code></td><td>тряска камеры</td></tr>
|
||||
<tr><td><code>focusOn(ref)</code></td><td>навести на объект</td></tr>
|
||||
<tr><td><code>cutscene(точки, опции)</code></td><td>пролёт камеры</td></tr>
|
||||
<tr><td><code>reset()</code></td><td>вернуть игроку</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><b>Пример — облёт уровня при старте игры:</b></p>
|
||||
<ScriptKind kind="global" />
|
||||
<Code>{`// камера плавно пролетает через три точки
|
||||
<Code>{`// Облёт уровня при старте
|
||||
game.camera.cutscene([
|
||||
{ x: 0, y: 20, z: -30 },
|
||||
{ x: 0, y: 15, z: 0 },
|
||||
{ x: 0, y: 10, z: 30 }
|
||||
], { segDuration: 2 }); // 2 секунды на отрезок
|
||||
], { segDuration: 2 });
|
||||
|
||||
// когда облёт закончится — отдать камеру игроку
|
||||
game.onCutsceneDone(() => {
|
||||
game.ui.showText('Поехали!', 2);
|
||||
});`}</Code>
|
||||
</>}
|
||||
lua={<>
|
||||
<p>В Lua — стандартный Roblox Camera через Workspace.CurrentCamera:</p>
|
||||
<table className="docTable">
|
||||
<tbody>
|
||||
<tr><td><code>camera.FieldOfView = 90</code></td><td>угол обзора</td></tr>
|
||||
<tr><td><code>camera.CameraType = Enum.CameraType.Scriptable</code></td><td>отключить авто-следование</td></tr>
|
||||
<tr><td><code>camera.CFrame = CFrame.new(pos, look)</code></td><td>поставить камеру</td></tr>
|
||||
<tr><td><code>TweenService:Create(camera, ...)</code></td><td>плавный пролёт</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<Code>{`local TweenService = game:GetService("TweenService")
|
||||
local camera = workspace.CurrentCamera
|
||||
|
||||
-- Отключаем авто-следование за игроком
|
||||
camera.CameraType = Enum.CameraType.Scriptable
|
||||
|
||||
-- Облёт через 3 точки за 6 секунд (3 этапа по 2 сек)
|
||||
local points = {
|
||||
Vector3.new(0, 20, -30),
|
||||
Vector3.new(0, 15, 0),
|
||||
Vector3.new(0, 10, 30),
|
||||
}
|
||||
|
||||
for _, point in ipairs(points) do
|
||||
local goal = { CFrame = CFrame.new(point, Vector3.new(0, 5, 0)) }
|
||||
local tween = TweenService:Create(camera, TweenInfo.new(2), goal)
|
||||
tween:Play()
|
||||
tween.Completed:Wait()
|
||||
end
|
||||
|
||||
-- Вернуть камеру игроку
|
||||
camera.CameraType = Enum.CameraType.Custom
|
||||
print("Поехали!")`}</Code>
|
||||
</>}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -2780,22 +2931,37 @@ game.onCutsceneDone(() => {
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
Отдел <code>game.fx</code> создаёт красивые эффекты-линии:
|
||||
<b>Beam</b> — светящаяся линия между двумя точками
|
||||
(лазер, мост света), <b>Trail</b> — шлейф за движущимся
|
||||
объектом (след за ракетой).
|
||||
</p>
|
||||
<ScriptKind kind="global" />
|
||||
<Code>{`// Лазер между двумя башнями
|
||||
<LangTabs
|
||||
js={<Code>{`// Лазер между двумя башнями
|
||||
const t1 = game.scene.findOne('Башня1');
|
||||
const t2 = game.scene.findOne('Башня2');
|
||||
|
||||
const laser = game.fx.beam({
|
||||
from: t1,
|
||||
to: t2,
|
||||
color: '#ff3344',
|
||||
width: 0.3
|
||||
});`}</Code>
|
||||
from: t1, to: t2,
|
||||
color: '#ff3344', width: 0.3
|
||||
});`}</Code>}
|
||||
lua={<Code>{`-- В Roblox Beam — это инстанс на Attachment'е
|
||||
local t1 = workspace:WaitForChild("Башня1")
|
||||
local t2 = workspace:WaitForChild("Башня2")
|
||||
|
||||
-- Attachment'ы — точки на Part'ах
|
||||
local a0 = Instance.new("Attachment", t1)
|
||||
local a1 = Instance.new("Attachment", t2)
|
||||
|
||||
local beam = Instance.new("Beam")
|
||||
beam.Attachment0 = a0
|
||||
beam.Attachment1 = a1
|
||||
beam.Color = ColorSequence.new(Color3.fromRGB(255, 51, 68))
|
||||
beam.Width0 = 0.3
|
||||
beam.Width1 = 0.3
|
||||
beam.LightEmission = 1
|
||||
beam.Parent = t1`}</Code>}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -2804,40 +2970,62 @@ const laser = game.fx.beam({
|
||||
title: 'G6. Мультиплеер: игроки, комната, команды',
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
В Рублоксе можно сделать игру на несколько игроков
|
||||
в одной комнате. Главные отделы:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<code>game.players</code> — список игроков:
|
||||
<code> all()</code>, <code>count()</code>,
|
||||
<code> me()</code> (это я);
|
||||
</li>
|
||||
<li>
|
||||
<code>game.room</code> — общее состояние комнаты,
|
||||
которое видят все игроки;
|
||||
</li>
|
||||
<li>
|
||||
<code>game.teams</code> — команды.
|
||||
</li>
|
||||
</ul>
|
||||
<p>В Рублоксе можно сделать игру на несколько игроков.</p>
|
||||
<ScriptKind kind="global" />
|
||||
<Code>{`// Общий счёт команды — виден всем игрокам в комнате
|
||||
<LangTabs
|
||||
js={<>
|
||||
<p>В JS — отделы <code>game.players</code>, <code>game.room</code>, <code>game.teams</code>:</p>
|
||||
<Code>{`// Общий счёт команды — виден всем игрокам
|
||||
game.room.set('totalScore', 0);
|
||||
|
||||
// когда счёт меняется — обновляем надпись у всех
|
||||
// когда счёт меняется — обновляем надпись
|
||||
game.room.onChange('totalScore', (val) => {
|
||||
game.ui.set('score', 'Счёт команды: ' + val);
|
||||
});
|
||||
|
||||
// сколько игроков сейчас в игре
|
||||
game.log('Игроков в комнате:', game.players.count());
|
||||
game.log('Игроков:', game.players.count());
|
||||
|
||||
// когда новый игрок зашёл
|
||||
game.onPlayerJoin((p) => {
|
||||
game.ui.showText(p.name + ' присоединился!', 2);
|
||||
});`}</Code>
|
||||
</>}
|
||||
lua={<>
|
||||
<p>В Lua — стандартный Roblox-стиль:</p>
|
||||
<Code>{`local Players = game:GetService("Players")
|
||||
local Teams = game:GetService("Teams")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
-- Общий счёт команды через NumberValue в ReplicatedStorage
|
||||
-- (виден всем игрокам через .Changed)
|
||||
local totalScore = Instance.new("NumberValue")
|
||||
totalScore.Name = "TotalScore"
|
||||
totalScore.Value = 0
|
||||
totalScore.Parent = ReplicatedStorage
|
||||
|
||||
totalScore.Changed:Connect(function(newValue)
|
||||
print("Счёт команды:", newValue)
|
||||
end)
|
||||
|
||||
print("Игроков:", #Players:GetPlayers())
|
||||
|
||||
-- Когда новый игрок зашёл
|
||||
Players.PlayerAdded:Connect(function(player)
|
||||
print(player.Name .. " присоединился!")
|
||||
end)`}</Code>
|
||||
<p><b>Команды (Teams):</b></p>
|
||||
<Code>{`-- Команды создают в Teams сервисе или скриптом
|
||||
local redTeam = Instance.new("Team")
|
||||
redTeam.Name = "Red"
|
||||
redTeam.TeamColor = BrickColor.new("Bright red")
|
||||
redTeam.AutoAssignable = true
|
||||
redTeam.Parent = game:GetService("Teams")
|
||||
|
||||
-- Назначить игрока в команду
|
||||
Players.PlayerAdded:Connect(function(player)
|
||||
player.Team = redTeam
|
||||
end)`}</Code>
|
||||
</>}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -2847,29 +3035,66 @@ game.onPlayerJoin((p) => {
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
<b>Лидерборд</b> — таблица очков игроков справа-сверху (как
|
||||
в Roblox). Объяви стат и меняй значение:
|
||||
<b>Лидерборд</b> — таблица очков справа-сверху (как в Roblox).
|
||||
</p>
|
||||
<ScriptKind kind="global" />
|
||||
<LangTabs
|
||||
js={<>
|
||||
<Code>{`game.leaderstats.define('Монеты', { initial: 0, icon: 'coin' });
|
||||
game.leaderstats.define('Уровень', { initial: 1 });
|
||||
|
||||
game.leaderstats.me.add('Монеты', 5); // +5 текущему игроку
|
||||
game.leaderstats.me.set('Уровень', 2); // задать значение
|
||||
game.leaderstats.me.add('Монеты', 5);
|
||||
game.leaderstats.me.set('Уровень', 2);
|
||||
const c = game.leaderstats.me.get('Монеты');`}</Code>
|
||||
<p>
|
||||
<b>Достижения</b> — всплывающие ачивки с редкостью и звуком:
|
||||
</p>
|
||||
<p><b>Достижения</b>:</p>
|
||||
<Code>{`game.achievements.define([
|
||||
{ id: 'first_coin', name: 'Первая монетка', description: 'Собери монету', icon: 'coin', rarity: 'common' },
|
||||
{ id: 'first_coin', name: 'Первая монетка', icon: 'coin', rarity: 'common' },
|
||||
{ id: 'rich', name: 'Богач', description: '100 монет', icon: 'trophy', rarity: 'legendary' }
|
||||
]);
|
||||
game.achievements.unlock('first_coin');
|
||||
// или авто-разблокировка по статy:
|
||||
game.achievements.bindToStat('rich', 'Монеты', 100);`}</Code>
|
||||
</>}
|
||||
lua={<>
|
||||
<p>В Lua — стандартный Roblox-паттерн: создаём папку
|
||||
<code> leaderstats</code> в Player с IntValue внутри:</p>
|
||||
<Code>{`local Players = game:GetService("Players")
|
||||
|
||||
Players.PlayerAdded:Connect(function(player)
|
||||
-- Папка leaderstats — Roblox автоматически показывает её в HUD
|
||||
local stats = Instance.new("Folder")
|
||||
stats.Name = "leaderstats"
|
||||
stats.Parent = player
|
||||
|
||||
-- Стат "Монеты"
|
||||
local coins = Instance.new("IntValue")
|
||||
coins.Name = "Монеты"
|
||||
coins.Value = 0
|
||||
coins.Parent = stats
|
||||
|
||||
-- Стат "Уровень"
|
||||
local level = Instance.new("IntValue")
|
||||
level.Name = "Уровень"
|
||||
level.Value = 1
|
||||
level.Parent = stats
|
||||
end)
|
||||
|
||||
-- Добавить монеты текущему игроку
|
||||
local function addCoins(player, amount)
|
||||
local stats = player:FindFirstChild("leaderstats")
|
||||
if stats then
|
||||
stats.Монеты.Value = stats.Монеты.Value + amount
|
||||
end
|
||||
end`}</Code>
|
||||
<p>
|
||||
Папка с именем <code>leaderstats</code> на игроке —
|
||||
магическое имя в Roblox. Любые IntValue/NumberValue/StringValue
|
||||
внутри неё автоматически попадают в HUD справа сверху.
|
||||
</p>
|
||||
</>}
|
||||
/>
|
||||
<Note>
|
||||
Лидерборд и достижения сохраняются в БД и подтягиваются при
|
||||
следующем входе игрока.
|
||||
следующем входе игрока (DataStoreService в Roblox).
|
||||
</Note>
|
||||
</>
|
||||
),
|
||||
@ -2880,16 +3105,61 @@ game.achievements.bindToStat('rich', 'Монеты', 100);`}</Code>
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
Всплывающие <b>цифры урона</b> над врагом — как в RPG. Самый
|
||||
простой способ — авто-режим (цифры над всеми мобами при уроне):
|
||||
Всплывающие <b>цифры урона</b> над врагом — как в RPG.
|
||||
</p>
|
||||
<ScriptKind kind="global" />
|
||||
<Code>{`game.fx.autoMobFloaters(true);`}</Code>
|
||||
<p>Ручной вызов в нужный момент:</p>
|
||||
<Code>{`game.fx.damageFloater(enemy.position, 25); // обычный урон
|
||||
game.fx.damageFloater(enemy.position, 100, { isCrit: true }); // крит — крупно, жёлтый
|
||||
game.fx.damageFloater('player', 30, { isHeal: true }); // лечение, зелёный
|
||||
game.fx.damageFloater(pos, 0, { isMiss: true }); // промах MISS`}</Code>
|
||||
<LangTabs
|
||||
js={<>
|
||||
<p>В JS это готовая команда:</p>
|
||||
<Code>{`game.fx.autoMobFloaters(true); // авто для всех мобов
|
||||
|
||||
// или вручную
|
||||
game.fx.damageFloater(enemy.position, 25);
|
||||
game.fx.damageFloater(enemy.position, 100, { isCrit: true });
|
||||
game.fx.damageFloater('player', 30, { isHeal: true });
|
||||
game.fx.damageFloater(pos, 0, { isMiss: true });`}</Code>
|
||||
</>}
|
||||
lua={<>
|
||||
<p>В Lua делаем сами через BillboardGui + TweenService:</p>
|
||||
<Code>{`local TweenService = game:GetService("TweenService")
|
||||
|
||||
local function showDamage(position, amount, isCrit)
|
||||
-- Невидимый Part-якорь в нужной точке
|
||||
local anchor = Instance.new("Part")
|
||||
anchor.Anchored = true
|
||||
anchor.CanCollide = false
|
||||
anchor.Transparency = 1
|
||||
anchor.Size = Vector3.new(0.1, 0.1, 0.1)
|
||||
anchor.Position = position + Vector3.new(0, 2, 0)
|
||||
anchor.Parent = workspace
|
||||
|
||||
-- BillboardGui над якорем
|
||||
local bg = Instance.new("BillboardGui", anchor)
|
||||
bg.Size = UDim2.new(3, 0, 1, 0)
|
||||
bg.AlwaysOnTop = true
|
||||
|
||||
local label = Instance.new("TextLabel", bg)
|
||||
label.Size = UDim2.new(1, 0, 1, 0)
|
||||
label.BackgroundTransparency = 1
|
||||
label.Text = "-" .. amount
|
||||
label.TextColor3 = isCrit
|
||||
and Color3.fromRGB(255, 200, 0) -- жёлтый крит
|
||||
or Color3.fromRGB(255, 80, 80) -- красный обычный
|
||||
label.TextScaled = true
|
||||
label.Font = Enum.Font.GothamBold
|
||||
|
||||
-- Анимируем вверх + исчезание
|
||||
local goal = { Position = anchor.Position + Vector3.new(0, 4, 0) }
|
||||
TweenService:Create(anchor, TweenInfo.new(1), goal):Play()
|
||||
|
||||
task.delay(1, function() anchor:Destroy() end)
|
||||
end
|
||||
|
||||
-- Пример использования:
|
||||
showDamage(workspace.Враг.Position, 25, false)
|
||||
showDamage(workspace.Враг.Position, 100, true) -- крит`}</Code>
|
||||
</>}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -2898,11 +3168,10 @@ game.fx.damageFloater(pos, 0, { isMiss: true }); // промах MISS`}</
|
||||
title: 'G9. Предметы и инвентарь с редкостями',
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
Полноценный инвентарь (сетка + хотбар, стаки, редкости).
|
||||
Сначала опиши предметы, потом выдавай:
|
||||
</p>
|
||||
<ScriptKind kind="global" />
|
||||
<LangTabs
|
||||
js={<>
|
||||
<p>В JS — готовый отдел <code>game.items</code> и <code>game.inventory</code>:</p>
|
||||
<Code>{`game.items.define([
|
||||
{ id: 'berry', name: 'Ягоды', emoji: '🍓', rarity: 'common', maxStack: 16 },
|
||||
{ id: 'potion', name: 'Зелье', emoji: '🧪', rarity: 'rare', maxStack: 8, onUseEffect: 'heal:50' },
|
||||
@ -2910,18 +3179,57 @@ game.fx.damageFloater(pos, 0, { isMiss: true }); // промах MISS`}</
|
||||
]);
|
||||
|
||||
game.inventory.give('sword', 1);
|
||||
game.inventory.give('berry', 5); // стак`}</Code>
|
||||
<p>Сбор предмета с земли (скрипт на предмете):</p>
|
||||
<ScriptKind kind="object" on="собираемый предмет" />
|
||||
game.inventory.give('berry', 5);`}</Code>
|
||||
<p>Сбор предмета с земли:</p>
|
||||
<Code>{`game.self.onInteract(() => {
|
||||
game.inventory.give('berry', 2);
|
||||
game.self.delete();
|
||||
}, { text: 'Собрать', key: 'e', distance: 3 });`}</Code>
|
||||
}, { text: 'Собрать', distance: 3 });`}</Code>
|
||||
<Note>
|
||||
Редкости: common (серый), uncommon (зелёный), rare (голубой),
|
||||
epic (фиолетовый), legendary (золотой). Окно инвентаря —
|
||||
клавиша <kbd className="kbd">I</kbd>, drag-drop, ПКМ-меню.
|
||||
</Note>
|
||||
</>}
|
||||
lua={<>
|
||||
<p>
|
||||
В Roblox инвентарь — это <code>Backpack</code> игрока с Tool'ами,
|
||||
плюс свои <code>IntValue</code>'ы для подсчёта стаков.
|
||||
Готового «инвентаря с редкостями» нет — собирается из частей:
|
||||
</p>
|
||||
<Code>{`-- Пример: ягоды как IntValue в leaderstats
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
Players.PlayerAdded:Connect(function(player)
|
||||
local stats = Instance.new("Folder")
|
||||
stats.Name = "leaderstats"
|
||||
stats.Parent = player
|
||||
|
||||
local berries = Instance.new("IntValue", stats)
|
||||
berries.Name = "Ягоды"
|
||||
berries.Value = 0
|
||||
end)
|
||||
|
||||
-- Сбор ягод (скрипт на собираемом Part)
|
||||
local part = script.Parent
|
||||
local prompt = Instance.new("ProximityPrompt")
|
||||
prompt.ActionText = "Собрать"
|
||||
prompt.Parent = part
|
||||
|
||||
prompt.Triggered:Connect(function(player)
|
||||
local berries = player.leaderstats and player.leaderstats:FindFirstChild("Ягоды")
|
||||
if berries then
|
||||
berries.Value = berries.Value + 2
|
||||
part:Destroy()
|
||||
end
|
||||
end)`}</Code>
|
||||
<p>
|
||||
Для полноценной системы с редкостями, иконками и UI окном —
|
||||
надо собирать ScreenGui вручную (по статье C). Это много кода —
|
||||
проще использовать JS-вариант с готовым <code>game.items</code>.
|
||||
</p>
|
||||
</>}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -2930,18 +3238,52 @@ game.inventory.give('berry', 5); // стак`}</Code>
|
||||
title: 'G10. Небо, облака, туман, время суток',
|
||||
body: (
|
||||
<>
|
||||
<p>Кастомное небо одной строкой — пресеты:</p>
|
||||
<ScriptKind kind="global" />
|
||||
<LangTabs
|
||||
js={<>
|
||||
<p>Пресеты неба одной командой:</p>
|
||||
<Code>{`game.scene.setSkybox({ preset: 'sunset' });
|
||||
// пресеты: clear-summer-day | lowpoly-roblox | cloudy | sunset | starry-night | space`}</Code>
|
||||
<p>Облака, туман и плавный переход:</p>
|
||||
<Code>{`game.scene.setClouds({ enabled: true, cover: 0.5, speed: 0.02 });
|
||||
// пресеты: clear-summer-day | lowpoly-roblox | cloudy | sunset | starry-night | space
|
||||
|
||||
game.scene.setClouds({ enabled: true, cover: 0.5, speed: 0.02 });
|
||||
game.scene.setFog({ color: '#dddddd', density: 0.006 });
|
||||
game.scene.skybox.fadeTo({ preset: 'starry-night' }, 3); // плавно за 3 сек`}</Code>
|
||||
<p>Простое управление цветом неба и временем суток:</p>
|
||||
<Code>{`game.environment.setSkyColor('#0a1024'); // тёмное небо
|
||||
game.environment.setTimeOfDay(0); // ночь (0..24)
|
||||
game.scene.skybox.fadeTo({ preset: 'starry-night' }, 3);
|
||||
|
||||
game.environment.setSkyColor('#0a1024');
|
||||
game.environment.setTimeOfDay(0); // ночь
|
||||
game.environment.setTimeOfDay(12); // полдень`}</Code>
|
||||
</>}
|
||||
lua={<>
|
||||
<p>В Roblox небо — это инстансы <code>Sky</code> и <code>Atmosphere</code>
|
||||
в Lighting:</p>
|
||||
<Code>{`local Lighting = game:GetService("Lighting")
|
||||
|
||||
-- Sky-инстанс с собственными текстурами
|
||||
local sky = Instance.new("Sky")
|
||||
sky.SkyboxBk = "rbxassetid://..." -- задняя грань
|
||||
sky.SkyboxFt = "rbxassetid://..." -- передняя
|
||||
sky.SkyboxLf = "rbxassetid://..." -- левая
|
||||
sky.SkyboxRt = "rbxassetid://..." -- правая
|
||||
sky.SkyboxUp = "rbxassetid://..." -- верх
|
||||
sky.SkyboxDn = "rbxassetid://..." -- низ
|
||||
sky.Parent = Lighting
|
||||
|
||||
-- Туман
|
||||
Lighting.FogColor = Color3.fromRGB(221, 221, 221)
|
||||
Lighting.FogStart = 50
|
||||
Lighting.FogEnd = 500
|
||||
|
||||
-- Atmosphere (мгла, плотность)
|
||||
local atmosphere = Instance.new("Atmosphere")
|
||||
atmosphere.Density = 0.3
|
||||
atmosphere.Color = Color3.fromRGB(199, 199, 199)
|
||||
atmosphere.Parent = Lighting
|
||||
|
||||
-- Время суток (часы и минуты от полуночи)
|
||||
Lighting:SetMinutesAfterMidnight(12 * 60) -- полдень
|
||||
Lighting:SetMinutesAfterMidnight(0) -- полночь`}</Code>
|
||||
</>}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -2950,32 +3292,91 @@ game.environment.setTimeOfDay(12); // полдень`}</Code>
|
||||
title: 'G11. Диалоги, меню, экран загрузки',
|
||||
body: (
|
||||
<>
|
||||
<p><b>Диалог NPC</b> построчно:</p>
|
||||
<ScriptKind kind="global" />
|
||||
<LangTabs
|
||||
js={<>
|
||||
<p><b>Диалог NPC</b>:</p>
|
||||
<Code>{`game.modal.dialog('Староста', [
|
||||
'Привет, путник!',
|
||||
'Собери 10 монет и возвращайся.',
|
||||
], () => game.ui.showText('Квест начат!', 2));`}</Code>
|
||||
<p><b>Окно Да/Нет</b> и <b>лутбокс</b>:</p>
|
||||
<Code>{`game.modal.confirmation('Выход', 'Точно выйти?', () => game.player.respawn(), null);
|
||||
<Code>{`game.modal.confirmation('Выход', 'Точно выйти?',
|
||||
() => game.player.respawn(), null);
|
||||
|
||||
game.modal.lootbox([
|
||||
{ name: 'Меч', color: '#f0ad4e', rarity: 'legendary' },
|
||||
{ name: 'Щит', color: '#5bc0de', rarity: 'rare' },
|
||||
], (item) => game.ui.showText('Выпал: ' + item.name, 3));`}</Code>
|
||||
<p><b>Экран загрузки</b> при переходе между уровнями:</p>
|
||||
<p><b>Экран загрузки</b>:</p>
|
||||
<Code>{`game.loading.show({
|
||||
style: 'ken-burns',
|
||||
placeName: 'Глава 2 — Шахта',
|
||||
studioName: 'Моя студия',
|
||||
duration: 2
|
||||
});
|
||||
game.loading.onHide(() => game.ui.showText('Добро пожаловать!', 2));`}</Code>
|
||||
<Note>
|
||||
Стартовый экран загрузки игры настраивается без кода —
|
||||
см. раздел вики «Экран загрузки» (карточка в разборе игр) и
|
||||
вкладку «Стартовый экран» в настройках проекта.
|
||||
</Note>
|
||||
</>}
|
||||
lua={<>
|
||||
<p>
|
||||
В Roblox/Lua нет готовых модалок — всё собирается через
|
||||
<b> ScreenGui</b> с Frame'ами. Это много кода (~30-100 строк
|
||||
на диалог), но полностью кастомизируется.
|
||||
</p>
|
||||
<Code>{`-- Простейший диалог: ScreenGui + Frame + TextLabel + Button
|
||||
local Players = game:GetService("Players")
|
||||
local player = Players.LocalPlayer
|
||||
local gui = player:WaitForChild("PlayerGui")
|
||||
|
||||
local function showDialog(speaker, lines, onDone)
|
||||
local screen = Instance.new("ScreenGui", gui)
|
||||
local frame = Instance.new("Frame", screen)
|
||||
frame.Size = UDim2.new(0.6, 0, 0.25, 0)
|
||||
frame.Position = UDim2.new(0.2, 0, 0.65, 0)
|
||||
frame.BackgroundColor3 = Color3.new(0, 0, 0)
|
||||
frame.BackgroundTransparency = 0.4
|
||||
|
||||
local nameLabel = Instance.new("TextLabel", frame)
|
||||
nameLabel.Size = UDim2.new(1, 0, 0.2, 0)
|
||||
nameLabel.Text = speaker
|
||||
nameLabel.TextColor3 = Color3.fromRGB(255, 220, 100)
|
||||
nameLabel.BackgroundTransparency = 1
|
||||
|
||||
local textLabel = Instance.new("TextLabel", frame)
|
||||
textLabel.Size = UDim2.new(1, -20, 0.6, 0)
|
||||
textLabel.Position = UDim2.new(0, 10, 0.2, 0)
|
||||
textLabel.TextColor3 = Color3.new(1, 1, 1)
|
||||
textLabel.BackgroundTransparency = 1
|
||||
textLabel.TextWrapped = true
|
||||
|
||||
local idx = 1
|
||||
local function showLine()
|
||||
textLabel.Text = lines[idx]
|
||||
end
|
||||
|
||||
local btn = Instance.new("TextButton", frame)
|
||||
btn.Size = UDim2.new(0.3, 0, 0.15, 0)
|
||||
btn.Position = UDim2.new(0.65, 0, 0.82, 0)
|
||||
btn.Text = "Дальше"
|
||||
|
||||
btn.MouseButton1Click:Connect(function()
|
||||
idx = idx + 1
|
||||
if idx > #lines then
|
||||
screen:Destroy()
|
||||
if onDone then onDone() end
|
||||
else
|
||||
showLine()
|
||||
end
|
||||
end)
|
||||
|
||||
showLine()
|
||||
end
|
||||
|
||||
showDialog("Староста", {
|
||||
"Привет, путник!",
|
||||
"Собери 10 монет и возвращайся.",
|
||||
}, function() print("Квест начат!") end)`}</Code>
|
||||
</>}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -2984,14 +3385,14 @@ game.loading.onHide(() => game.ui.showText('Добро пожаловать!', 2
|
||||
title: 'G12. Машины и главное меню',
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
<b>Машина</b>, на которой можно ездить (вход hold-F, WASD руль):
|
||||
</p>
|
||||
<ScriptKind kind="global" />
|
||||
<LangTabs
|
||||
js={<>
|
||||
<p><b>Машина</b>, на которой можно ездить (вход hold-F, WASD руль):</p>
|
||||
<Code>{`game.scene.spawn('vehicle:car', { x: 0, y: 1, z: 0, name: 'Тачка' });
|
||||
game.onVehicleEnter(() => game.ui.showText('За рулём! WASD — ехать', 2));
|
||||
game.onVehicleExit(() => game.ui.showText('Вышел', 1));`}</Code>
|
||||
<p><b>Главное меню</b> игры с живой камерой и кнопкой ИГРАТЬ:</p>
|
||||
<p><b>Главное меню</b>:</p>
|
||||
<Code>{`game.mainMenu.show({
|
||||
title: 'МОЯ ИГРА',
|
||||
camera: 'orbit',
|
||||
@ -2999,6 +3400,34 @@ game.onVehicleExit(() => game.ui.showText('Вышел', 1));`}</Code>
|
||||
patchNotes: { title: 'Что нового', items: ['Добавлены машины', 'Новая карта'] },
|
||||
onPlay: () => game.ui.showText('Поехали!', 2)
|
||||
});`}</Code>
|
||||
</>}
|
||||
lua={<>
|
||||
<p>
|
||||
В Roblox машина — это сложная Model с VehicleSeat
|
||||
внутри. Когда игрок садится в VehicleSeat — у него
|
||||
появляются <code>.Throttle</code> и <code>.Steer</code>
|
||||
свойства от WASD автоматически:
|
||||
</p>
|
||||
<Code>{`-- VehicleSeat внутри Model
|
||||
local seat = workspace:WaitForChild("Тачка"):WaitForChild("VehicleSeat")
|
||||
|
||||
-- Слушаем игрока в кресле
|
||||
seat:GetPropertyChangedSignal("Occupant"):Connect(function()
|
||||
if seat.Occupant then
|
||||
print("За рулём! WASD — ехать")
|
||||
else
|
||||
print("Вышел")
|
||||
end
|
||||
end)
|
||||
|
||||
-- Throttle (W/S) и Steer (A/D) — автоматически в seat.Throttle и seat.Steer
|
||||
-- Применяй их к скорости/повороту в RunService.Heartbeat`}</Code>
|
||||
<p>
|
||||
Главное меню — собирается через ScreenGui (см. C2-C3),
|
||||
или используется готовый StarterGui от Roblox.
|
||||
</p>
|
||||
</>}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user