「前編のコースはできたけれど、敵や体力がないとゲームらしくない」「集めたコインが、次に遊んだときに0に戻ってしまう」
ロブロックス(Roblox)スタジオで、前編で作った夜のアスレチックゲーム「ナイトラン」を完成させる方法を、実際の画像付きで誰でも簡単に分かるように解説します。後編では、追いかけてくる敵、体力ゲージ、回復アイテム、Shiftキーのダッシュ、次に遊んだときも残るコイン、コインとゴールの効果音を足します。
中級ステップ28/28 前の記事:ロブロックス(Roblox)のアスレチックゲームの作り方【前編】 全体の順番を見る
使うもの:Roblox Studioが起動するパソコン、マウス、キーボード、前編で作ってRobloxに保存した作品。1つずつの仕組みは、それぞれの記事でくわしく解説しています。この記事では、前編のゲームへのつなぎ方を中心に進めます。
今回の完成状態:下の画像のように、紫の床に乗ると敵が追いかけてきて(画像の1)、ぶつかると画面の下の体力ゲージが減れば(画像の2)、ゲームの山場ができています。最後にゴールして、止めたあとにもう一度遊ぶと、前回のコインの数から始まれば完成です。

後編で足すもの
前編のコースのどこに、どの仕組みを足すかを先に決めておきます。
| 足すもの | 置く場所 | 使う仕組み | くわしい記事 |
|---|---|---|---|
| 敵の床と敵 | Checkpoint2とゴールの間 | 決めた床の上の人だけを追う敵 | 追いかけてくる敵の作り方 |
| 体力ゲージ | 画面の下 | ScreenGuiと、体力に合わせて長さを変えるLocalScript | 体力ゲージと回復アイテム |
| 回復アイテム | 敵の床の角に2つ | フォルダーの中の箱に、まとめて回復の動きを付ける | 体力ゲージと回復アイテム |
| ダッシュ | プレイヤー側 | Shiftキーを押している間速く走る | カメラとダッシュ |
| コインの保存 | 前編のCoinService | DataStoreで、抜けたときに保存して、入ったときに読み込む | DataStoreの使い方 |
| 効果音 | SoundService | コインを拾ったときと、ゴールしたときに鳴らす | 音楽と効果音の入れ方 |
前編のコースは足場が細く、まわりが溶岩です。そこで敵は、溶岩に落ちないように、広い床を1枚用意して、その上でだけ追いかけるようにします。
1. 前編の作品を開く
Roblox Studioを起動し、「最近使用したバーチャル空間」の「Asobishift Night Run」(赤い枠)を押します。一覧に出ていないときは、上の「ファイル」メニューの「Robloxから開く」から選びます。

前編の最後と同じ、夜の溶岩の上に足場が並んだ作品が開きます。

チーム制作がオンの作品は、変更が自動でクラウドに保存されます。この記事の手順の間、「Robloxに保存」を押し直す必要はありません。
2. 溶岩を広げて、ゴールを奥へ動かす
敵の床を置く場所を空けるため、溶岩を奥へ広げて、ゴールを奥へ動かします。
エクスプローラの「Workspace」の「Course」の中の「Lava」を押して選びます。プロパティの検索欄に「Size」と入力し、Sizeに「40, 1, 113」と入力します(赤い枠)。前編の88から、奥へ25スタッド長くします。

Lavaを選んだまま、プロパティの検索欄に「Position」と入力し、CFrameの中のPositionに「0, 0.6, -63.5」と入力します(赤い枠)。溶岩の手前の端は前編と同じ場所のまま、奥にだけ広がります。

エクスプローラの「Course」の中の「Goal」を押して選びます。プロパティの検索欄に「Position」と入力し、CFrameの中のPositionに「0, 10, -106」と入力します(画像の1)。ゴールが奥へ動きます(画像の2)。

3. 敵の床を置く
エクスプローラの「Course」にマウスを乗せて、右に出る「+」を押します(赤い枠)。

一覧の検索欄に「Part」と入れ、一覧の「Part」を押します(赤い枠)。

Courseの中にPartが入ります。エクスプローラで「Part」を押して選び、F2キーを押して名前を「EnemyFloor」に変えます(赤い枠)。敵のスクリプトは、この名前で床を探します。

EnemyFloorを選んだまま、プロパティの検索欄に「Size」と入力し、Sizeに「20, 1, 24」と入力します(赤い枠)。前編の足場(6×6)より大きくして、敵から逃げ回れる広さにします。

プロパティの検索欄に「Position」と入力し、CFrameの中のPositionに「0, 9, -89」と入力します(画像の1)。Checkpoint2とゴールの間に、大きな床が入ります(画像の2)。

プロパティの検索欄に「Color」と入力し、Colorに「110, 70, 160」と入力します(赤い枠)。紫にして、ほかの足場と区別します。

プロパティの検索欄に「Anchored」と入力し、Anchoredにチェックを入れます(赤い枠)。チェックがないと、テストを始めたときに床が溶岩へ落ちます。

4. 敵を置く
上の「ホーム」タブの「キャラクター」(画像の1)を押し、出てきた一覧の「ブロック型アバター(2012年)」(画像の2)を押します。

エクスプローラに「Rig」が入り(画像の1)、3D画面にキャラクターが置かれます(画像の2)。

エクスプローラで「Rig」を押して選び、F2キーを押して名前を「Enemy」に変えます(赤い枠)。

Enemyを選んだまま、プロパティの検索欄に「Origin」と入力します。Originの中のPositionに「0, 9.5, -95」、Orientationに「0, 180, 0」と入力します(赤い枠)。敵が紫の床の奥に立ち、スタートのほうを向きます。

Orientationは「-0, 180, -0」と表示されることがあります。0と同じ意味なので、そのままで大丈夫です。
エクスプローラの「Enemy」の左の「▶」を押して開き、中の「Humanoid」を押して選びます。プロパティの検索欄に「DisplayName」と入力し、DisplayNameに「敵」と入力します(画像の1)。頭の上の名前が「敵」になります(画像の2)。

5. 床の上だけ追いかけるスクリプトを入れる
エクスプローラの「Workspace」の左の「▼」を押して閉じます。「ServerScriptService」にマウスを乗せて、右に出る「+」を押します(赤い枠)。

一覧の検索欄に「Script」と入れ、一覧の「Script」を押します(赤い枠)。

エクスプローラで「Script」を押して選び、F2キーを押して名前を「EnemyService」に変えます(赤い枠)。

開いているEnemyServiceの画面で、中身を全部消して、次のコードを貼り付けます。
local Players = game:GetService("Players")
local enemy = workspace:WaitForChild("Enemy")
local humanoid = enemy:WaitForChild("Humanoid")
local root = enemy:WaitForChild("HumanoidRootPart")
local floor = workspace:WaitForChild("Course"):WaitForChild("EnemyFloor")
local DAMAGE = 20 -- 1回に減らす体力
local COOLDOWN = 1 -- 同じ人にもう一度当たるまでの秒数
local home = root.Position -- 最初に立っていた場所
humanoid.WalkSpeed = 12 -- プレイヤー(16)より少し遅くして、逃げられるようにする
local ok = pcall(function()
root:SetNetworkOwner(nil)
end)
if not ok then
warn("Enemy のパーツの Anchored を外してください")
end
-- 位置が EnemyFloor の上にあるか(敵が溶岩に落ちないよう、床の上の人だけ追う)
local function isOnFloor(position)
local localPos = floor.CFrame:PointToObjectSpace(position)
return math.abs(localPos.X) < floor.Size.X / 2 and math.abs(localPos.Z) < floor.Size.Z / 2
end
local function findTarget()
local nearest = nil
local nearestDistance = math.huge
for _, player in Players:GetPlayers() do
local character = player.Character
local targetHumanoid = character and character:FindFirstChildOfClass("Humanoid")
local targetRoot = character and character:FindFirstChild("HumanoidRootPart")
if targetHumanoid and targetRoot and targetHumanoid.Health > 0 and isOnFloor(targetRoot.Position) then
local distance = (targetRoot.Position - root.Position).Magnitude
if distance < nearestDistance then
nearest = targetRoot
nearestDistance = distance
end
end
end
return nearest
end
local lastHit = {}
local function onTouched(hit)
if humanoid.Health <= 0 then return end
local character = hit.Parent
local player = Players:GetPlayerFromCharacter(character)
if not player then return end
local targetHumanoid = character:FindFirstChildOfClass("Humanoid")
if not targetHumanoid or targetHumanoid.Health <= 0 then return end
local now = os.clock()
if lastHit[player] and now - lastHit[player] < COOLDOWN then return end
lastHit[player] = now
targetHumanoid:TakeDamage(DAMAGE)
end
for _, part in enemy:GetDescendants() do
if part:IsA("BasePart") then
part.Touched:Connect(onTouched)
end
end
Players.PlayerRemoving:Connect(function(player)
lastHit[player] = nil
end)
-- 床の上に人がいれば追いかけ、いなければ最初の場所へ戻る
while humanoid.Health > 0 do
local target = findTarget()
if target then
humanoid:MoveTo(target.Position)
else
humanoid:MoveTo(home)
end
task.wait(0.2)
end

追いかけてくる敵の作り方のコードは、60スタッド以内のいちばん近い人へ、まっすぐ歩いていきます。そのままこのコースで使うと、敵は床の端から溶岩の上へ下りてしまいます。そこで、追いかける相手を「EnemyFloorの上にいる人」だけにしました。
| 前の記事から変えたところ | コードの場所 | していること |
|---|---|---|
| 床の上か確かめる | isOnFloor | 位置を床から見た向きに直し、床の横幅と奥行きの半分より内側なら「床の上」とする |
| 追う相手を選ぶ | findTarget | 床の上にいて、生きている人の中から、いちばん近い人を選ぶ |
| 誰もいないとき | humanoid:MoveTo(home) | その場で止まるのではなく、最初に立っていた場所へ戻る |
床から下りた人を追いかけないので、Checkpoint2へ逃げれば敵は戻っていきます。逃げ場があるから、プレイヤーは「どこで戦うか」を選べるようになります。
6. 体力ゲージの枠を作る
エクスプローラの「ServerScriptService」の左の「▼」を押して閉じます。「StarterGui」にマウスを乗せて、右に出る「+」を押します(赤い枠)。

一覧の検索欄に「ScreenGui」と入れ、一覧の「ScreenGui」を押します(赤い枠)。

StarterGuiの中に、前編のGoalGuiと並んでScreenGuiが入ります。エクスプローラで「ScreenGui」を押して選び、F2キーを押して名前を「HealthGui」に変えます(赤い枠)。

エクスプローラの「HealthGui」にマウスを乗せて「+」を押し、一覧の検索欄に「Frame」と入れて、一覧の「Frame」を押します(赤い枠)。

エクスプローラで「Frame」を押して選び、F2キーを押して名前を「MeterBar」に変えます。MeterBarを選んだまま、プロパティの検索欄に「AnchorPoint」と入力し、AnchorPointに「0.5, 1」と入力します(赤い枠)。位置を決めるときの基準が、四角の「下の真ん中」になります。

プロパティの検索欄に「Position」と入力し、Positionに「0.5, 0, 1, -30」と入力します(赤い枠)。四角が画面の下の真ん中に移ります。

プロパティの検索欄に「Size」と入力し、Sizeに「0, 300, 0, 28」と入力します(画像の1)。横300ピクセル、縦28ピクセルの細長い帯になります(画像の2)。

プロパティの検索欄に「BackgroundColor3」と入力し、BackgroundColor3に「40, 40, 40」と入力します(赤い枠)。帯が黒っぽくなります。体力が減ったときに見える、ゲージの空いた部分の色です。

数字の意味(AnchorPointやPositionの4つの数)は、体力ゲージと回復アイテムの6章でくわしく解説しています。
7. 緑の帯と体力の数字を入れる
エクスプローラの「MeterBar」にマウスを乗せて、右に出る「+」を押します(赤い枠)。

一覧の検索欄に「Frame」と入れ、一覧の「Frame」を押します(赤い枠)。

MeterBarの中にFrameが入ります。エクスプローラで「Frame」を押して選び、F2キーを押して名前を「Fill」に変えます(赤い枠)。

Fillを選んだまま、プロパティの検索欄に「Size」と入力し、Sizeに「1, 0, 1, 0」と入力します(赤い枠)。FillがMeterBarの中いっぱいに広がります。

プロパティの検索欄に「BackgroundColor3」と入力し、BackgroundColor3に「60, 200, 90」と入力します(画像の1)。帯が緑になります(画像の2)。

もう一度、エクスプローラの「MeterBar」の「+」を押し、一覧の検索欄に「TextLabel」と入れて、一覧の「TextLabel」を押します(赤い枠)。

エクスプローラで「TextLabel」を押して選び、F2キーを押して名前を「HealthText」に変えます(赤い枠)。

HealthTextを選んだまま、プロパティを1つずつ変えます。どれも、プロパティの検索欄に名前を入力してから、値を入れます。
| プロパティ | 入れる値 | 画像 |
|---|---|---|
| Size | 1, 0, 1, 0 | 下の1枚目 |
| BackgroundTransparency | 1 | 下の2枚目(文字の後ろの白い四角が消える) |
| Text | 100 / 100 | 下の3枚目 |
| TextColor3 | 255, 255, 255 | 下の4枚目(文字が白になる) |
| TextScaled | チェックを入れる | 下の5枚目(文字が帯の高さに合わせて大きくなる) |
| ZIndex | 2 | 下の6枚目(文字が緑の帯に隠れなくなる) |
Textの「100 / 100」は、編集画面で見た目を確かめるための仮の文字です。テストを始めると、次のスクリプトが本当の体力に書きかえます。
8. ゲージを動かすスクリプトを入れる
エクスプローラの「HealthGui」にマウスを乗せて「+」を押し、一覧の検索欄に「LocalScript」と入れて、一覧の「LocalScript」を押します(赤い枠)。

エクスプローラで「LocalScript」を押して選び、F2キーを押して名前を「HealthBar」に変えます(赤い枠)。

開いているHealthBarの画面で、中身を全部消して、次のコードを貼り付けます。体力ゲージと回復アイテムと同じコードです。
local Players = game:GetService("Players")
local StarterGui = game:GetService("StarterGui")
local gui = script.Parent
local meter = gui:WaitForChild("MeterBar")
local fill = meter:WaitForChild("Fill")
local label = meter:WaitForChild("HealthText")
-- Robloxの最初からある体力ゲージ(右上)をかくす
StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Health, false)
local RED = Color3.fromRGB(230, 60, 60)
local GREEN = Color3.fromRGB(60, 200, 90)
local function update(humanoid)
local ratio = math.clamp(humanoid.Health / humanoid.MaxHealth, 0, 1)
fill.Size = UDim2.fromScale(ratio, 1) -- 体力の割合だけ、緑の帯をのばす
fill.BackgroundColor3 = RED:Lerp(GREEN, ratio) -- 減るほど赤に近づける
label.Text = math.ceil(humanoid.Health) .. " / " .. humanoid.MaxHealth
end
local function onCharacter(character)
local humanoid = character:WaitForChild("Humanoid")
update(humanoid)
humanoid.HealthChanged:Connect(function()
update(humanoid)
end)
end
local player = Players.LocalPlayer
if player.Character then
onCharacter(player.Character)
end
player.CharacterAdded:Connect(onCharacter)

体力ゲージのスクリプトは、どこで体力が減ったかを気にしません。前編の溶岩で倒れたときも、敵にぶつかったときも、同じゲージが動きます。
9. 回復アイテムを2つ置く
3D画面の上の作品のタブを押して3D画面に戻ります。エクスプローラの「Workspace」にマウスを乗せて、右に出る「+」を押します(赤い枠)。

一覧の検索欄に「Folder」と入れ、一覧の「Folder」を押します(赤い枠)。

エクスプローラで「Folder」を押して選び、F2キーを押して名前を「HealthPickups」に変えます(赤い枠)。

エクスプローラの「HealthPickups」にマウスを乗せて「+」を押し、一覧の検索欄に「Part」と入れて、一覧の「Part」を押します(赤い枠)。

HealthPickupsの中にPartが入ります。エクスプローラで「Part」を押して選び、F2キーを押して名前を「HealthPack」に変えます(赤い枠)。

HealthPackを選んだまま、プロパティの検索欄に「Size」と入力し、Sizeに「2, 2, 2」と入力します(赤い枠)。

プロパティの検索欄に「Position」と入力し、CFrameの中のPositionに「7, 11, -81」と入力します(画像の1)。紫の床の手前の右の角に、箱が浮かびます(画像の2)。

続けてHealthPackを選んだまま、次の4つを変えます。どれもプロパティの検索欄に名前を入力してから変えます。
| プロパティ | 入れる値 | 理由 |
|---|---|---|
| Color | 60, 220, 100 | 回復アイテムだと分かる緑にする(下の1枚目) |
| Material | Neon | 光らせて、夜でも見つけやすくする(下の2枚目) |
| Anchored | チェックを入れる | 浮かせたまま止めておく(下の3枚目) |
| CanCollide | チェックを外す | ぶつからずに通り抜けながら取れるようにする(下の4枚目) |
エクスプローラで「HealthPack」を押して選び、Ctrl + Dキーを押します。同じ設定の「HealthPack」がもう1つできます(赤い枠)。

2つ目のHealthPackが選ばれたまま、プロパティの検索欄に「Position」と入力し、CFrameの中のPositionに「-7, 11, -99」と入力します(画像の1)。紫の床の奥の左の角に、2つ目の箱が移ります(画像の2)。

手前と奥の角に1つずつ置くと、敵をかわしながら箱まで走る遊び方ができます。
10. 回復のスクリプトを入れる
エクスプローラの「Workspace」の左の「▼」を押して閉じます。「ServerScriptService」の「+」を押し、一覧の検索欄に「Script」と入れて、一覧の「Script」を押します(赤い枠)。

エクスプローラで「Script」を押して選び、F2キーを押して名前を「PickupManager」に変えます(赤い枠)。

開いているPickupManagerの画面で、中身を全部消して、次のコードを貼り付けます。体力ゲージと回復アイテムと同じコードです。
local HEAL = 30 -- 1つで回復する体力
local RESPAWN_TIME = 5 -- 取ってから元に戻るまでの秒数
local pickupsFolder = workspace:WaitForChild("HealthPickups")
local function onTouchPickup(hit, pickup)
if pickup:GetAttribute("Used") then
return
end
local humanoid = hit.Parent:FindFirstChildWhichIsA("Humanoid")
if not humanoid or humanoid.Health <= 0 or humanoid.Health >= humanoid.MaxHealth then
return
end
-- 上限(MaxHealth)をこえないように回復する
humanoid.Health = math.min(humanoid.Health + HEAL, humanoid.MaxHealth)
pickup:SetAttribute("Used", true)
pickup.Transparency = 0.8 -- うすくして、取ったことを分かるようにする
task.wait(RESPAWN_TIME)
pickup.Transparency = 0
pickup:SetAttribute("Used", false)
end
-- フォルダーの中の回復アイテムに、まとめて同じ動きを付ける
for _, pickup in pickupsFolder:GetChildren() do
pickup.Touched:Connect(function(hit)
onTouchPickup(hit, pickup)
end)
end

1つ取ると体力が30戻り、うすくなった箱は5秒後に元に戻ります。体力が満タンのときは取らない作りです。
11. Studioのテストでコインを保存できるようにする
コインを保存する仕組み(DataStore)は、最初の設定のままだとStudioのテストで使えません。上の「ファイル」メニューを開き、「バーチャル空間設定」(赤い枠)を押します。

バーチャル空間設定の画面が開きます。左の「セキュリティ」(赤い枠)を押します。

「StudioのAPIサービスへのアクセスを有効化」のスイッチ(赤い枠)は、最初はオフです。設定の画面を開いたとき、下の出力の欄にオレンジの「Failed to parse secrets」という文字が出ることがあります。Studioの設定の画面の中で出る表示で、作品のスクリプトとは関係ありません。

スイッチを押してオンにし(画像の1)、右下の「保存」(画像の2)を押します。

Robloxの公式の説明によると、Studioのテストは、公開しているゲームと同じ保存データを読み書きします。たくさんの人が遊んでいるゲームでは、このスイッチをオンにしないでください。テストで本番のデータを書きかえてしまうおそれがあるからです。この記事の作品はまだ公開していないので、オンにして大丈夫です。
12. コインのスクリプトに保存を足す
エクスプローラの「ServerScriptService」の中の、前編で作った「CoinService」(赤い枠)をダブルクリックして開きます。

開いたCoinServiceの画面で、中身を全部消して、次のコードを貼り付けます。前編のコイン集めの仕組みに、DataStoreの使い方の保存と読み込みを足したものです。
local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("NightRunCoins")
local coins = workspace:WaitForChild("Coins")
local RESPAWN_SECONDS = 5 -- 拾われたコインが戻るまでの秒数
local loaded = {} -- 読み込みに成功した人だけ true
local saving = 0 -- 今、保存している途中の数
-- 参加した人ごとに、保存してあるコインの数を読み込んで、右上の Coins を用意する
local function addLeaderstats(player)
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
local count = Instance.new("IntValue")
count.Name = "Coins"
count.Value = 0
count.Parent = leaderstats
local ok, saved = pcall(function()
return store:GetAsync("player_" .. player.UserId)
end)
if ok then
count.Value = saved or 0
loaded[player] = true
else
-- 読み込みに失敗したときは保存しない(前のデータを 0 で上書きしないため)
warn("Coinsの読み込みに失敗しました", saved)
end
leaderstats.Parent = player -- 数を入れてから右上に出す
end
local function save(player)
if not loaded[player] then return end
loaded[player] = nil -- 同じ人を2回保存しないよう、先に印を外す
saving += 1
local value = player.leaderstats.Coins.Value
local ok, err = pcall(function()
store:SetAsync("player_" .. player.UserId, value)
end)
saving -= 1
if ok then
print(player.Name, "のCoinsを保存しました", value)
else
warn("Coinsの保存に失敗しました", err)
end
end
Players.PlayerAdded:Connect(addLeaderstats)
for _, player in Players:GetPlayers() do
addLeaderstats(player)
end
Players.PlayerRemoving:Connect(save)
-- サーバーが閉じるときは、残っている人の分も保存し、保存が終わるまで待つ
game:BindToClose(function()
for _, player in Players:GetPlayers() do
task.spawn(save, player)
end
while saving > 0 do
task.wait()
end
end)
local function setupCoin(coin)
local ready = true
coin.Touched:Connect(function(hit)
if not ready then
return
end
local player = Players:GetPlayerFromCharacter(hit.Parent)
if not player or not player:FindFirstChild("leaderstats") then
return
end
ready = false
player.leaderstats.Coins.Value += 1
coin.Transparency = 1
task.wait(RESPAWN_SECONDS)
coin.Transparency = 0
ready = true
end)
end
for _, coin in coins:GetChildren() do
setupCoin(coin)
end

| 前編から足したところ | コードの場所 | していること |
|---|---|---|
| 保存場所を決める | GetDataStore("NightRunCoins") | このゲームのコインを入れる場所に名前を付ける |
| 入ったときに読み込む | store:GetAsync | 前回のコインの数を読み、右上のCoinsに入れる。初めての人は0 |
| 数を入れてから出す | leaderstats.Parent = player | 読み込みが終わってから右上に出す。0から急に数が変わって見えるのを防ぐ |
| 抜けたときに保存する | Players.PlayerRemoving | 今のコインの数を書きこむ |
| サーバーが閉じるときにも保存する | game:BindToClose | 残っている人の分を保存し、保存が終わるまで待つ |
| 2回保存しない | loaded[player] = nil | 1人を保存したら印を外し、同じ人をもう一度保存しないようにする |
BindToClose は、Robloxの公式の説明で、DataStoreを使うなら一緒に使うよう書かれている仕組みです。サーバーは閉じる前に最大30秒待ってくれるので、その間に保存を終わらせます。
最後の「2回保存しない」は、アソビシフト編集部が試して足したところです。最初は抜けたときとサーバーが閉じるときの両方で同じ人を保存していて、テストを止めるたびに出力にオレンジの「DataStore request was added to queue」が出ました。同じ人を続けて書きこむと、順番待ちになるという意味です。1人1回だけ保存する形に直すと、この表示は出なくなりました。
13. Shiftキーのダッシュを入れる
エクスプローラの「StarterPlayer」の左の「▶」を押して開き、中の「StarterPlayerScripts」にマウスを乗せて、右に出る「+」を押します(赤い枠)。

一覧の検索欄に「LocalScript」と入れ、一覧の「LocalScript」を押します(赤い枠)。

エクスプローラで「LocalScript」を押して選び、F2キーを押して名前を「DashControl」に変えます(赤い枠)。

開いているDashControlの画面で、中身を全部消して、次のコードを貼り付けます。カメラとダッシュと同じコードです。
local ContextActionService = game:GetService("ContextActionService")
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local camera = workspace.CurrentCamera
local DASH_ACTION = "Dash"
local NORMAL_SPEED = 16 -- ふだんの歩く速さ(最初の値)
local DASH_SPEED = 28 -- Shiftキーを押している間の速さ
local NORMAL_FOV = 70 -- ふだんの見える広さ(最初の値)
local DASH_FOV = 80 -- 走っている間は、少し広く見せる
local function onDash(actionName, inputState, inputObject)
local character = player.Character
local humanoid = character and character:FindFirstChildWhichIsA("Humanoid")
if not humanoid then
return
end
if inputState == Enum.UserInputState.Begin then
humanoid.WalkSpeed = DASH_SPEED
camera.FieldOfView = DASH_FOV
elseif inputState == Enum.UserInputState.End then
humanoid.WalkSpeed = NORMAL_SPEED
camera.FieldOfView = NORMAL_FOV
end
end
-- 左Shiftキーに「Dash」をつなぐ。true にすると、スマホでは画面にボタンが出る
ContextActionService:BindAction(DASH_ACTION, onDash, true, Enum.KeyCode.LeftShift)
ContextActionService:SetTitle(DASH_ACTION, "ダッシュ")

敵の歩く速さは12、プレイヤーは16、ダッシュ中は28です。ふつうに歩いても逃げられるけれど、ダッシュすれば大きく引き離せる速さにしています。
14. 効果音を探す
上の「ホーム」タブの「ツールボックス」(画像の1)を押すと、左にツールボックスが開きます。「カテゴリ」の中の「オーディオ」(画像の2)を押します。

上の検索欄に「coin」と入れてEnterキーを押します(画像の1)。一覧のいちばん上の「Coin Collect」(画像の2)が、コインを拾った音です。名前にマウスを乗せると左に出る再生ボタンで、試しに聞けます。

「Coin Collect」を右クリックして、「アセットIDをコピー」(赤い枠)を押します。音の番号(ID)がコピーされます。メモ帳などに貼って、取っておきます。

同じように、検索欄に「victory」と入れてEnterキーを押します(画像の1)。一覧のいちばん上の「victory.wav」(画像の2)が、ゴールしたときの音です。作った人の欄が「Roblox」になっている、Robloxの公式の音です。

「victory.wav」も右クリックして「アセットIDをコピー」(赤い枠)を押し、IDを取っておきます。

| 使う音 | 検索した言葉 | 音の名前 | コピーしたID |
|---|---|---|---|
| コインを拾った音 | coin | Coin Collect | 135483737426662 |
| ゴールの音 | victory | victory.wav | 12222253 |
検索の結果の順番は変わることがあります。同じ音が見つからないときは、上の表のIDをそのまま使えます。ツールボックスは、右上の「×」で閉じておきます。
15. 効果音を入れる
エクスプローラの「SoundService」にマウスを乗せて、右に出る「+」を押します(赤い枠)。

一覧の検索欄に「Sound」と入れ、一覧の「Sound」を押します(赤い枠)。

エクスプローラで「Sound」を押して選び、F2キーを押して名前を「CoinSound」に変えます(赤い枠)。

CoinSoundを選んだまま、プロパティの検索欄に「SoundId」と入力し、SoundIdに「rbxassetid://135483737426662」と入力します(赤い枠)。「rbxassetid://」のあとに、コピーしたIDをつなげます。

エクスプローラで「CoinSound」を押して選び、Ctrl + Dキーを押します。2つ目の「CoinSound」ができます(赤い枠)。

2つ目のCoinSoundを選び、F2キーを押して名前を「GoalSound」に変えます(赤い枠)。

GoalSoundを選んだまま、プロパティの検索欄に「SoundId」と入力し、SoundIdを「rbxassetid://12222253」に書きかえます(赤い枠)。

16. 効果音を鳴らすスクリプトを入れる
エクスプローラの「StarterPlayerScripts」の「+」を押し、一覧の検索欄に「LocalScript」と入れて、一覧の「LocalScript」を押します(赤い枠)。

エクスプローラで「LocalScript」を押して選び、F2キーを押して名前を「GameSounds」に変えます(赤い枠)。

開いているGameSoundsの画面で、中身を全部消して、次のコードを貼り付けます。
local Players = game:GetService("Players")
local SoundService = game:GetService("SoundService")
local player = Players.LocalPlayer
local coinSound = SoundService:WaitForChild("CoinSound")
local goalSound = SoundService:WaitForChild("GoalSound")
-- コインが増えたときだけ鳴らす(自分の画面だけで鳴る)
local coinsValue = player:WaitForChild("leaderstats"):WaitForChild("Coins")
local last = coinsValue.Value
coinsValue.Changed:Connect(function(value)
if value > last then
coinSound:Play()
end
last = value
end)
-- ゴールした印が付いたら鳴らす
player:GetAttributeChangedSignal("Cleared"):Connect(function()
if player:GetAttribute("Cleared") then
goalSound:Play()
end
end)

| 行 | していること |
|---|---|
coinsValue.Changed | 右上のCoinsの数が変わるたびに動く |
if value > last then | 数が増えたときだけ鳴らす |
GetAttributeChangedSignal("Cleared") | 前編のCourseServiceが付ける「ゴールした印」を見て、ゴールの音を鳴らす |
LocalScriptで鳴らしているので、音は拾った本人の画面でだけ鳴ります。ほかの人がコインを拾っても、自分には聞こえません。音の聞こえ方の違いは、音楽と効果音の入れ方で解説しています。
12章で、読み込みが終わってから右上にCoinsを出すようにしたのは、この音のためでもあります。読み込みの前に0で出してしまうと、前回の数が入ったときに「数が増えた」と判定されて、コインの音が鳴ってしまいます。
17. テストで最初から最後まで遊ぶ
3D画面の上の作品のタブを押して3D画面に戻り、F5キーでテストを始めます。右上のCoinsは0から始まり(画像の1)、画面の下に体力ゲージ「100 / 100」が出ます(画像の2)。
右上の名前の欄はモザイクにしています。

コインに触ると、コインの音が鳴ります。前編と同じように足場を渡って、Checkpoint2まで進みます。Checkpoint2にいる間、敵は紫の床の奥で待っています(画像の1)。3枚とも拾っていれば、Coinsは3です(画像の2)。

紫の床に下りると、敵が追いかけてきます。ぶつかると体力が20減り、ゲージが短くなります(赤い枠)。

緑の箱に触ると体力が30戻り、箱がうすくなります(赤い枠)。画像では、敵に2回ぶつかって60になったあと、箱で90まで戻り、そこから自然に少し増えて92になっています。

Shiftキーを押している間は速く走り、見える範囲が少し広がります。下の2枚は、同じ場所でShiftキーを押す前(上)と、押している間(下)です。押している間は、紫の床やまわりの溶岩が小さく、広く見えます。


敵をかわしてゴールに乗ると、ゴールの音が鳴って「ゴール!おめでとう」が出ます(画像の1)。画像では、ゴールの前にもう1枚拾い直したので、Coinsは4です(画像の2)。

Shift + F5キーでテストを停止します。出力に「(アカウント名)のCoinsを保存しました 4」と出れば、保存できています(赤い枠)。

もう一度F5キーでテストを始めます。右上のCoinsが、0ではなく前回の4から始まります(赤い枠)。

| 試したこと | 結果 |
|---|---|
| テストを始める | Coinsは0、体力ゲージは100 / 100 |
| コインに触る | コインの音が鳴り、Coinsが1増える |
| Checkpoint2にいる | 敵は紫の床の奥で待ったまま、追ってこない |
| 紫の床に下りる | 敵が追いかけてきて、ぶつかるたびに体力が20減る(約1秒に1回まで) |
| 緑の箱に触る | 体力が30戻る。満タンのときは取らない |
| Shiftキーを押す | 速く走り、見える範囲が広がる。離すと元に戻る |
| ゴールに乗る | ゴールの音が鳴り、「ゴール!おめでとう」が出る |
| 停止して、もう一度始める | 停止したときの出力に「保存しました」。次のテストは前回のCoinsの数から始まる |
確かめ終わったら、Shift + F5キーでテストを停止します。
ゲームを自分らしく変える
EnemyService・PickupManager・DashControlは、コードの中の数字です。エクスプローラでその名前のスクリプトをダブルクリックして開き、表の名前の行の数字を書き換えます。
最後の「CoinSound・GoalSoundのVolume」はプロパティです。エクスプローラでSoundServiceの中の「CoinSound」を押して選び、プロパティの検索欄に「Volume」と入力して、右の欄に「0.3」と入力します。GoalSoundも同じように変えます。
| 変えたいこと | 変える場所 | 例 |
|---|---|---|
| 敵を速くする | EnemyServiceの humanoid.WalkSpeed | 12 → 14。16にすると、ダッシュしないと逃げられない |
| 敵の攻撃を弱くする | EnemyServiceの DAMAGE | 20 → 10 |
| 回復を多くする | PickupManagerの HEAL | 30 → 50 |
| ダッシュを速くする | DashControlの DASH_SPEED | 28 → 32 |
| 効果音を小さくする | CoinSound・GoalSoundのVolume | 0.5 → 0.3 |
アシスタントに頼むときは、部品の名前とスクリプトの名前を書くと、思ったとおりに直してもらいやすくなります。
Roblox Studioで、ServerScriptServiceの「EnemyService」が、Workspaceの「Enemy」を動かしています。
Enemyは、Courseフォルダーの「EnemyFloor」の上にいるプレイヤーだけを追いかけます。
敵が床の上を、プレイヤーがいないときは左右に行ったり来たりするようにしてください。
EnemyFloorの外には出ないようにしてください。
公開する前に確かめること
この記事では、作品を公開していません。遊んでもらうには「ファイル」メニューの「Robloxに公開」と、Creator Hubでの公開範囲の設定が必要です。手順と、公開できないときの条件は、テストと公開の方法で解説しています。
公開の前に、次の4つを確かめておくと安心です。
| 確かめること | 理由 |
|---|---|
| 最初から最後まで、一度も止まらずに遊べる | 足場の間が遠すぎる、敵が強すぎるなど、遊ぶ人が詰まるところを先に直す |
| 出力に赤い文字が出ていない | スクリプトの止まりは、遊ぶ人の画面では分からない |
| スマホの画面でも遊べる | デバイスシミュレータ(3D画面の右上の歯車のアイコンの「デバイスシミュレーション」)で確かめる。ダッシュのボタンは画面に出る |
| 「StudioのAPIサービスへのアクセスを有効化」をどうするか決める | 公開して人が遊ぶようになったら、本番のデータを守るためにオフにする |
動かないときの確かめ方
| 症状 | 見るところ | 直し方 |
|---|---|---|
| 敵が動かない | EnemyのAnchored | Enemyの中のHumanoidRootPartのAnchoredのチェックを外す。出力に「Enemy のパーツの Anchored を外してください」と出る |
| 敵が追ってこない | 立っている場所 | 紫の床(EnemyFloor)の上にいるか。名前がEnemyFloorか、Courseの中にあるか |
| 体力が減らない | テスト開始直後 | 生き返った直後は、まわりの青い守りの光が消えるまでダメージを受けない |
| ゲージが出ない | HealthGuiの場所 | StarterGuiの中にあるか。MeterBar・Fill・HealthTextの名前が合っているか |
| 出力にオレンジの「Coinsの読み込みに失敗しました」が出る | バーチャル空間設定 | 11章のスイッチがオンになっているか。作品がRobloxに保存されているか |
| Coinsが毎回0から始まる | 停止したときの出力 | 「保存しました」が出ているか。出ていなければ、作品がRobloxに保存されているか確かめる |
| 効果音が鳴らない | SoundId | 「rbxassetid://」のあとが数字だけか。CoinSound・GoalSoundがSoundServiceの中にあり、名前が合っているか |
| 出力に「Infinite yield possible」と出る | 名前 | 出力に書かれた名前の物があるか。この表示の読み方はInfinite yield possibleの直し方で解説しています |
よくある質問
停止したときに、オレンジの「DataStore request was added to queue」が出た
同じ人のデータを、短い間に続けて書きこんだときの表示です。この記事のコードは1人1回だけ保存するので、アソビシフト編集部が試したときは出なくなりました。前のコードのまま「抜けたとき」と「サーバーが閉じるとき」の両方で保存していると出ることがあります。12章のコードを貼り直してください。
コインの数を0に戻したい
CoinServiceの GetDataStore("NightRunCoins") の名前を、「NightRunCoins2」のように変えます。新しい保存場所になるので、Coinsは0から始まります。
中級はここまでです。次は上級(1/42)
中級の記事で、次のことができるようになりました。
- ScriptとLocalScriptの違いを、2人のテストで見て確かめます
- ゲームを抜けても進み具合が残るようにします
- ボタンからサーバーに合図を送る仕組みを作ります
- ゲームパスを作って売ります
- 何回でも買えるアイテム(開発者製品)を売ります
- 回る床と、行ったり来たりする床を作ります
- 乗ると消える床と、時間内だけ渡れる橋を作ります
- TweenServiceで、パーツや画面の枠をなめらかに動かします
- 話しかけると喋るNPCを作ります
- 追いかけてくる敵を作ります
- 手に持って使えるアイテムを作ります
- 剣で攻撃できるようにします
- クリックした場所へレーザーを撃って当てるブラスターを作ります
- 赤と青のチームに分けます
- ゴールした人にバッジを渡します
- お金をためて機械を買い足すタイクーンを作ります
- ゲームが重くなったときに、原因を測って直します
- 敵が道を歩き、タワーが撃つタワーディフェンスを作ります
- お金でタワーを買って、好きな場所に置けるようにします
- 朝・夕方・夜を作り、夜の街灯を光らせます
- 体力ゲージと、拾うと回復するアイテムを作ります
- 自分の画像を取り込み、看板とコインのアイコン付きスコアを作ります
- 一人称視点・横から見るカメラ・Shiftキーのダッシュを作ります
- AIが書いたスクリプトの変数・if・for・関数・イベントを読めるようにします
- if・elseif・while・配列・辞書など、Lua言語の書き方を覚えます
- 壁と屋根を組み、ドアと窓の穴を開けて家を建てます
- これまでの仕組みを組み合わせて、夜のアスレチックゲームを作ります(前編)
- 敵・体力・回復・ダッシュ・コインの保存・効果音を足して、アスレチックゲームを完成させます(後編)
次は、乗って上がるかごと呼びボタンで、1階と2階を行き来するエレベーターを作ります。
次に知りたいことを選ぶ
- 次のステップ:乗って上がるかごと呼びボタンで、1階と2階を行き来するエレベーターを作ります → ロブロックス(Roblox)のエレベーターの作り方
- 作ったゲームを公開したい → ロブロックス(Roblox)のゲームを公開する方法
- ゴールした人にバッジを渡したい → ロブロックス(Roblox)のバッジの作り方
- 保存の仕組みをくわしく知りたい → ロブロックス(Roblox)のDataStoreの使い方
- 前編に戻りたい → ロブロックス(Roblox)のアスレチックゲームの作り方【前編】









