Практикум — королевская битва на Roblox
Продолжение маршрута после обби: цикл лобби → отсчёт → бой → победитель, оружие с Raycast на клиенте для визуала и повторной проверкой на сервере. API — workspace:Raycast, Luau и task.*.
Что вы освоите
| Тема | Навык |
|---|---|
| Игровой цикл раундов | Lobby / Intermission / Combat / Winner |
| Минимум игроков | MIN_PLAYERS, ожидание в while |
| Таблица участников | competitors фиксируется на старт раунда |
| Конфиг оружия | ModuleScript Settings в Tool |
| Raycast | Клиент стреляет лучом; сервер сверяет угол и дистанцию |
| Headshot | Множитель урона по Head |
| Локальная репликация | Replicate RemoteEvent — трассер и звук |
Структура проекта
ServerScriptService
├── ServerHandler
│ ├── Data
│ ├── GameRunner
│ └── Weapons
ReplicatedStorage
├── Remotes
│ ├── Hit (RemoteEvent — заявка о попадании)
│ └── Replicate (RemoteEvent — VFX для всех)
├── Tools
│ └── Blaster (Tool + ModuleScript Settings + LocalScript ToolHandler)
Workspace
└── Effects (папка для временных Part трассеров)
StarterGui
└── RoundHUD (фаза, таймер)
Модуль GameRunner
--!strict
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RoundStatus = ReplicatedStorage.Remotes.RoundStatus :: RemoteEvent
local MIN_PLAYERS = 2
local INTERMISSION = 15
local ROUND_TIME = 300
local GameRunner = {}
local competitors: { Player } = {}
local function broadcast(phase: string, seconds: number?)
RoundStatus:FireAllClients(phase, seconds or 0)
end
local function fillCompetitors()
table.clear(competitors)
for _, p in Players:GetPlayers() do
table.insert(competitors, p)
end
end
function GameRunner.gameLoop()
while true do
broadcast("Waiting", 0)
repeat
task.wait(1)
until #Players:GetPlayers() >= MIN_PLAYERS
for t = INTERMISSION, 1, -1 do
broadcast("Intermission", t)
task.wait(1)
end
fillCompetitors()
for _, p in competitors do
p:LoadCharacter()
end
broadcast("Combat", ROUND_TIME)
local deadline = os.clock() + ROUND_TIME
while os.clock() < deadline do
local alive = 0
local winner: Player? = nil
for _, p in competitors do
local hum = p.Character and p.Character:FindFirstChildOfClass("Humanoid")
if hum and hum.Health > 0 then
alive += 1
winner = p
end
end
if alive <= 1 then
if winner then
broadcast("Winner", 0)
end
break
end
task.wait(0.5)
end
task.wait(8)
end
end
return GameRunner
Data для BR — те же Coins / Wins / Kills, что в обби; ключ DataStore смените, чтобы не смешивать тестовые данные.
Конфиг оружия
Tool/Settings (ModuleScript):
--!strict
export type GunSettings = {
fireMode: "SEMI" | "AUTO",
damage: number,
headshotMultiplier: number,
rateOfFire: number,
range: number,
}
return {
fireMode = "SEMI",
damage = 22,
headshotMultiplier = 2,
rateOfFire = 450,
range = 500,
} :: GunSettings
| Поле | Смысл |
|---|---|
rateOfFire | Выстрелов в минуту; пауза 60 / rateOfFire |
range | Длина луча в студах |
fireMode | AUTO — цикл при зажатой кнопке |
Клиент — ToolHandler и Raycast
--!strict
-- LocalScript в Tool
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Debris = game:GetService("Debris")
local player = Players.LocalPlayer
local mouse = player:GetMouse()
local tool = script.Parent :: Tool
local handle = tool:WaitForChild("Handle") :: BasePart
local settings = require(tool:WaitForChild("Settings"))
local Hit = ReplicatedStorage.Remotes.Hit :: RemoteEvent
local Replicate = ReplicatedStorage.Remotes.Replicate :: RemoteEvent
local equipped = false
local firing = false
tool.Equipped:Connect(function()
equipped = true
end)
tool.Unequipped:Connect(function()
equipped = false
firing = false
end)
local function castRay(): (Instance?, Vector3, Vector3, Vector3)
local char = player.Character
if not char then
return nil, Vector3.zero, Vector3.zero, Vector3.zero
end
local origin = handle.Position
local direction = (mouse.Hit.Position - origin).Unit * settings.range
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = { char, workspace.Effects }
local result = workspace:Raycast(origin, direction, params)
local pos = if result then result.Position else origin + direction
return result and result.Instance or nil, pos, direction, origin
end
local function drawTracer(origin: Vector3, pos: Vector3)
Replicate:FireServer(tool, origin, pos)
end
local function tryFire()
if not equipped or tool:FindFirstChild("Debounce") and (tool.Debounce :: BoolValue).Value then
return
end
local waitTime = 60 / settings.rateOfFire
local deb = tool:FindFirstChild("Debounce") :: BoolValue?
if deb then deb.Value = true end
task.delay(waitTime, function()
if deb then deb.Value = false end
end)
local hit, pos, direction, origin = castRay()
drawTracer(origin, pos)
if hit then
local relCFrame = hit.CFrame:ToObjectSpace(CFrame.new(pos))
Hit:FireServer(tool, hit, direction, origin, relCFrame)
end
end
mouse.Button1Down:Connect(function()
firing = true
if settings.fireMode == "SEMI" then
tryFire()
else
task.spawn(function()
while firing and equipped do
tryFire()
task.wait(60 / settings.rateOfFire)
end
end)
end
end)
mouse.Button1Up:Connect(function()
firing = false
end)
Replicate.OnClientEvent на всех клиентах создаёт короткий Part-трассер в workspace.Effects и удаляет через Debris.
Сервер — verifyHit и урон
Клиент присылает hit, origin, direction, relCFrame. Сервер не доверяет hit слепо:
--!strict
local Players = game:GetService("Players")
local Weapons = {}
local function angleBetween(a: Vector3, b: Vector3): number
local cos = math.clamp(a.Unit:Dot(b.Unit), -1, 1)
return math.acos(cos)
end
function Weapons.verifyHit(
shooter: Player,
tool: Tool,
claimedHit: Instance,
direction: Vector3,
origin: Vector3,
relCFrame: CFrame,
settings: any
): boolean
local char = shooter.Character
if not char or not tool:IsDescendantOf(char) then
return false
end
if direction.Magnitude < 0.01 then
return false
end
if (origin - char:GetPivot().Position).Magnitude > 12 then
return false
end
local toTarget = (claimedHit.Position - origin).Unit
if angleBetween(direction.Unit, toTarget) > math.rad(8) then
return false
end
local recomposed = claimedHit.CFrame:ToWorldSpace(relCFrame).Position
if (recomposed - claimedHit.Position).Magnitude > 2 then
return false
end
if (origin - claimedHit.Position).Magnitude > settings.range + 10 then
return false
end
return true
end
function Weapons.applyDamage(shooter: Player, hit: Instance, settings: any)
local model = hit:FindFirstAncestorOfClass("Model")
if not model then return end
local victim = Players:GetPlayerFromCharacter(model)
if not victim or victim == shooter then return end
local hum = model:FindFirstChildOfClass("Humanoid")
if not hum or hum.Health <= 0 then return end
local dmg = settings.damage
if hit.Name == "Head" then
dmg *= settings.headshotMultiplier
end
hum:TakeDamage(dmg)
-- increment Kills in Data if hum died
end
return Weapons
Подключение:
Hit.OnServerEvent:Connect(function(player, tool, hit, direction, origin, relCFrame)
local settings = require(tool:WaitForChild("Settings"))
if not Weapons.verifyHit(player, tool, hit, direction, origin, relCFrame, settings) then
return
end
Weapons.applyDamage(player, hit, settings)
end)
В продакшене часто делают второй workspace:Raycast на сервере из origin по direction и сравнивают цель с ответом клиента. Это дороже по CPU, но устойчивее к подмене hit.
Локальная репликация VFX
| Что | Где |
|---|---|
| Трассер, дым, звук выстрела | Клиент стрелка + Replicate для остальных |
TakeDamage | Только сервер |
| UI попадания | FireClient жертве после applyDamage |
Подробнее о репликации — Разработка на Roblox.
HUD раунда
RoundStatus.OnClientEvent обновляет TextLabel:
RoundStatus.OnClientEvent:Connect(function(phase: string, seconds: number)
if phase == "Intermission" then
label.Text = "Старт через " .. seconds
elseif phase == "Combat" then
label.Text = "Бой!"
elseif phase == "Winner" then
label.Text = "Победитель определён"
end
end)
Тестирование
| Сценарий | Ожидание |
|---|---|
| 1 игрок | Раунд ждёт MIN_PLAYERS |
| 2 клиента | Урон только через сервер; убийство засчитывается |
| Выход mid-round | competitors не ломает подсчёт живых |
| Headshot | Урон x headshotMultiplier только при hit.Name == "Head" |
Чек-лист
-
GameRunnerпроходит полный цикл Waiting → Combat → Winner -
Settingsв Tool,Debounceна Tool - Клиент использует
RaycastParams, не устаревшийFindPartOnRay -
verifyHitотсекает неверный угол и дистанцию - Урон и Kills только на сервере
- VFX через
Replicate, без локальногоTakeDamage
См. также
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). Разработка игр — это процесс создания видеоигр, который включает в себя множество этапов, от идеи и концепции до финального продукта. Термин * Разработка компьютерных и видеоигр — одна из наиболее интердисциплинарных и кооперативных областей в индустрии информационных технологий. Игровой движок как платформа - подсистемы рендеринга, физики, ввода и сценариев, ускоряющие создание видеоигр. Roblox Studio не является традиционным игровым движком, но представляет собой платформу как услугу (PaaS) с ограниченной, но эффективной средой разработки. Языки для игр на примере Unity - роль C#, стандартная библиотека, сборка мусора и продуктивность разработки под движок. Моделирование — это процесс создания трёхмерных объектов, называемых моделями, для последующего использования в цифровых средах, особенно в видеоиграх. Текстура в контексте разработки игр — это изображение, накладываемое на поверхность трёхмерной модели с целью придания ей визуальной детализации, цвета, рельефа и других свойств внешнего вида. Гейм-дизайн — три уровня опыта, MDA, механики и баланс; вход в углублённый маршрут и связь с Unity. Верхний уровень гейм-дизайна — субъективный опыт игрока, модели Bartle и Yee, персоны, вопросы experience design. Механики как строительные блоки гейм-дизайна — существительные и глаголы, пространство состояний, семейства control, progression, uncertainty, resource management. Как механики складываются в системы — цепочки конверсии, положительная и отрицательная обратная связь, эмерджентность и настройка баланса.Процесс разработки видеоигр
Дорожная карта геймдева
Команда разработки
Игровой движок
Виды игровых движков
Языки программирования игр
Моделирование
Текстуры
Гейм-дизайн
Опыт игрока и мотивационные модели
Механики и пространство состояний
Системы, петли обратной связи и баланс