🤖

RobloxでTycoonを作成するために、Zednov's Tycoon Kit を理解してみた

2023/08/28に公開

はじめに(記事の概要)

この記事では、Robloxで人気の"Zednov's Tycoon Kit"について解説します。このKitの使い方から中身の構成まで、Roblox初心者でも簡単にTycoonゲームを作れるようになる情報を提供できるよう頑張ります。

コンテキスト

  • Roblox Studioの基本的な使い方はわかる
  • プログラミングの基礎知識がある(Lua言語が理解できるとなお良い)

対象読者

  • Robloxの初心者
  • プログラミングはある程度理解している
  • RobloxでTycoonゲームを作ってみたいと思っている
  • どのスクリプトをWorkspaceやServerScriptServiceなどどこに配置するのか知りたい

セクション1: Zednov's Tycoon Kitとは?

Kitの概要

Zednov's Tycoon Kitは、RobloxでTycoonゲームを簡単に作成できるように設計されたフリーキットです。このキットを使用することで、プレイヤーは自分自身のTycoonゲームを手軽に作成できます。Lua言語を用いたスクリプトが組み込まれており、初心者からでも利用できます。

下記を使用しています。

https://create.roblox.com/marketplace/asset/4569838917/Zednovs-Tycoon-Kit-Fixed-by-NotedAPI

主な機能と特長

  1. プレイヤー管理: 新規プレイヤーのゲーム参加及び退出時の処理は自動化されています。
  2. 自動チーム作成: Tycoonsフォルダ内の各Tycoon(Fighting Bearsモデル)に基づき、自動的にチームが生成されます。
  3. ステータス管理: プレイヤーの資源や貨幣等、ゲーム内ステータスは容易に管理可能です。
  4. 所有権の自動設定: プレイヤーがTycoonに接触した際、そのTycoonの所有者として自動設定されます。
  5. Dev Productボタン: プレイヤーに特定の開発製品を購入させる機能が存在します。
  6. Gamepassボタン: 特定のGamepass(有料アイテム)を所有するプレイヤーのみが利用可能な機能を設定可能です。
  7. サウンド機能: ゲーム体験を高めるためのサウンド設定が用意されています。

このように、Zednov's Tycoon Kitは多くの便利な機能と特長を備えています。これにより、ゲーム作成がよりスムーズに、そして楽しくなります。

セクション2: Kitのインストール方法

Roblox Studioでのインストール手順

  1. Roblox Studioを開く: まずはRoblox Studioを開いて、新しいプロジェクトを作成または既存のプロジェクトを開きます。
  2. ツールバーからViewを選択: 上部のツールバーからViewをクリックします。
  3. Toolboxを開く: Viewタブの中からToolboxを選択して開きます。
  4. Zednov's Tycoon Kitを検索: Toolboxの検索バーにZednov's Tycoon Kitと入力して検索します。
  5. Kitをプロジェクトに追加: 検索結果からZednov's Tycoon Kitを見つけたら、それをクリックしてプロジェクトに追加します。

セクション3: Kitの中身の解説

Zednov's Tycoon Kitにはいくつかの主要なコンポーネントが含まれています。それぞれの役割と機能について詳しく見ていきましょう。

GameEssentials

このフォルダは、ゲームに必要な基本的な要素が含まれています。通常、プレイヤーのスポーン地点やゲーム内の基本的なオブジェクトがここに配置されます。
デフォルトでは何も入っておらず、空の状態です。
自分で作成するゲームの要素を格納するのだと思われます。

Tycoons

このフォルダには、各Tycoonのデータと設定が含まれています。プレイヤーが所有できる各Tycoonの設定やオブジェクトがここに保存されています。
初期ではFighting Bearsモデルが配置されており、中身は下記のようになっています。

Fighting Bears
 |-BuyObject :空。使われていなさそう
 |-CurrencyToCollect :(IntValueオブジェクト)取得前の稼いだ金額をプールしておく場所
 |-DropColor :DropItemの色設定
 |-MaterialValue :素材の質感(デフォルトはPlastic)
 |-Owner :所有者(プレイヤー)のユーザー名格納用オブジェクト
 |-PurchaseHandler :購入関連処理のスクリプト(詳細後術)
 |-TeamColor :チームの色(各タイクーンごとにチーム化している)
 |-Buttons :購入用ボタンのモデルが格納されている
 |-DevProductButton Example :DevProductの購入サンプル
 |-Entrance :入り口のゲート(通ると自分のTycoonとなる
 |-Essentials :初期配置される要素
 |-Gamepass Button:ゲームパス(有料アイテム)用ボタンのサンプル
 |-PurchasedObjects :ゲーム内で購入したオブジェクトが格納されていく
 |-Purchases :購入可能なオブジェクトが配置されている。PurchaseHnadlerスクリプトにて初期化時にメモリへコピーし、オブジェクトを削除している。

Core_handler (script)

配置箇所:Workspace > Zednov's Tycoon Kit > Core_Handler

CoreHandlerの要点

  1. 変数と設定: ゲームの基本設定と変数を初期化。
  2. チーム管理: 既存のチームカラーをチェックし、新しいチームを作成。
  3. タイクーン管理: プレイヤーが所有するタイクーンを特定。
  4. プレイヤーイベント: プレイヤーがゲームに参加・退出したときの処理。

DevProductHandler (script)

配置箇所:ServerScriptService > DevProductHandler

-- スクリプト内でServerScriptServiceへ配置している
script.Parent = game.ServerScriptService

DevProductHandlerの要点

  1. 初期設定: ゲームの設定とマーケットプレイスサービスを読み込む。
    https://create.roblox.com/docs/reference/engine/classes/MarketplaceService

  2. DevProductsの作成: タイクーン内のボタンからDevProduct情報を取得して、DevProductsテーブルに保存。

  3. レシート処理: MarketplaceServiceを使って、プレイヤーがDevProductを購入した際の処理を行う。

  4. プレイヤー認証: 購入したプレイヤーのIDを確認し、対応するDevProductを特定。

  5. オブジェクト生成: 購入に成功した場合、新しいオブジェクト(モデル)を生成して、プレイヤーのタイクーンに追加。

LinkedLeaderBoard (script)

配置箇所:ServerScriptService > LinkedLeaderboard

-- スクリプト内でServerScriptServiceへ配置している
script.Parent = game.ServerScriptService

LinkedLeaderBoardの要点

  1. 設定の読み込み: Settings モジュールを読み込む。
  2. 死亡イベント: プレイヤーが死んだときに、リーダーボードの「死亡数」を更新。
  3. キルカウント: プレイヤーが他のプレイヤーを倒したときに、リーダーボードの「キル数」を更新。
  4. プレイヤーのリスポーン: プレイヤーがリスポーンしたときに新しいHumanoidにイベントを接続。
  5. フラグスタンドの探索: ゲーム内のすべてのフラグスタンドを探して、リスナーを設定。
  6. キャプチャスコア: プレイヤーがフラグをキャプチャしたときに、リーダーボードの「キャプチャ数」を更新。
  7. プレイヤーの入場: 新しいプレイヤーがゲームに参加したときに、リーダーボードの各種スタッツを初期化。
  8. CTFモード: フラグスタンドが存在する場合、CTF(Capture The Flag)モードを有効にする。

Settings (module script table data)

このモジュールスクリプトは、ゲームの設定データを格納しています。ゲームの難易度や通貨の設定など、様々なゲーム内設定がここで管理されます。

READ ME (module script read me)

このモジュールスクリプトは、Kitの使用方法や設定方法についての説明が書かれています。初めてKitを使用する人にとって、非常に有用な情報が含まれています。

以上がZednov's Tycoon Kitの主要なコンポーネントとその機能です。これらを理解することで、より効率的にTycoonゲームを開発することができます。

セクション4: Scriptの内容

主要なスクリプト

Zednov's Tycoon Kit直下

  • Core_handler
  • DevProductHandler
  • LinkedLeaderBoard

Fighting Bears配下(各Tycoonのスクリプト)

  • PurchaseHnadler
  • GateControl

スクリプトの詳細

Core_handler (script)

概要

Core_handlerスクリプトは、Tycoonゲームの中心的な操作を制御します。プレイヤーの追加、Tycoonの生成、ステータスの管理など、多くの重要な処理がこのスクリプトで行われます。

コードセクションと解説
変数と設定: ゲームの基本設定と変数を初期化
-- Tycoonsという名前の空のテーブルを作成します。
-- このテーブルは後でTycoonオブジェクトを格納するために使用されます。
local Tycoons = {}

-- RobloxのTeamsサービスを取得して、Teamsという変数に格納します。
-- これで、後でチーム関連の処理が簡単に行えます。
local Teams = game:GetService('Teams')

-- 親スクリプトのSettingsモジュールをrequire関数で取得して、Settingsという変数に格納します。
-- これにより、設定情報に簡単にアクセスできます。
local Settings = require(script.Parent.Settings)

-- BrickColorクラスを短縮形としてBCという変数に格納します。
-- これで、後でBrickColor関連の処理が簡単に行えます。
local BC = BrickColor

-- 新しい'Folder'インスタンスを作成し、game.ServerStorageに格納します。
-- このフォルダは、プレイヤーのお金情報を保存するために使用されます。
local Storage = Instance.new('Folder', game.ServerStorage)

チーム管理: 既存のチームカラーをチェックし、新しいチームを作成
function returnColorTaken(color)
    -- Teamsサービスの子要素をループしてチェックします。
    for i,v in pairs(Teams:GetChildren()) do
        -- もし子要素が'Team'タイプなら、以下の処理を行います。
        if v:IsA('Team') then
            -- 与えられたカラーが既に存在するチームのカラーと一致するかどうかを確認します。
            if v.TeamColor == color then
                -- もし一致するカラーがあれば、trueを返します。
                return true
            end
        end
    end
    -- ループが終わっても一致するカラーがなければ、falseを返します。
    return false
end

この関数は、指定されたチームカラーが既に使用されているかどうかを確認します。

タイクーン管理: プレイヤーが所有するタイクーンを特定
for i,v in pairs(script.Parent:WaitForChild('Tycoons'):GetChildren()) do
    -- TycoonsテーブルにTycoonの名前とクローンを保存します。
    -- これは後でチームを作成する際に、Tycoonの名前をチーム名として使用するためです。
    Tycoons[v.Name] = v:Clone()

    -- 既に使われているチームカラーかどうかを確認します。
    -- Robloxでは同じチームカラーを持つチームは作成できないため、このチェックが必要です。
    if returnColorTaken(v.TeamColor) then
        -- 重複するチームカラーを処理します。
        
        local newColor;
        repeat
            wait()
            -- ランダムな新しいカラーを生成します。
            -- これは重複を避けるための処理です。
            newColor = BC.Random()
        until returnColorTaken(newColor) == false
        -- 新しいカラーをTycoonのTeamColorに設定します。
        v.TeamColor.Value = newColor
    end

    -- 重複がないことを確認した後で、新しいチームを作成します。
    -- これはプレイヤーがどのTycoonに所属しているのかを識別するためです。
    local NewTeam = Instance.new('Team',Teams)
    NewTeam.Name = v.Name
    NewTeam.TeamColor = v.TeamColor.Value

    -- 自動チーム割り当てが無効な場合、AutoAssignableをfalseにします。
    -- これはプレイヤーが自分でチームを選べるようにするための設定です。
    if not Settings['AutoAssignTeams'] then
        NewTeam.AutoAssignable = false
    end

    -- PurchaseHandlerスクリプトを有効にします。
    -- これはプレイヤーがアイテムを購入できるようにするための処理です。
    v.PurchaseHandler.Disabled = false
end

-- getPlrTycoon関数: 引数として与えられたプレイヤーが所有するTycoonを返す関数
function getPlrTycoon(player)
    -- script.Parent.Tycoonsの子要素を一つずつ調べる
    for i,v in pairs(script.Parent.Tycoons:GetChildren()) do
        -- 子要素が"Model"タイプであるか確認
        if v:IsA("Model") then
            -- ModelのOwnerプロパティが引数で与えられたプレイヤーと一致するか確認
            if v.Owner.Value == player then
                -- 一致した場合、そのTycoon(Model)を返す
                return v
            end
        end
    end
    -- 該当するTycoonが見つからなかった場合はnilを返す
    return nil
end
プレイヤーイベント: プレイヤーがゲームに参加・退出したときの処理

新規プレイヤーがゲームに参加したときや退出したときの処理がここで行われます。

-- プレイヤーがゲームに参加したときに発火するイベント
game.Players.PlayerAdded:connect(function(player)
    -- ServerStorageのPlayerMoneyにNumberValueを新規作成してプレイヤーのステータスとして保存
    local plrStats = Instance.new("NumberValue", game.ServerStorage.PlayerMoney)
    -- そのNumberValueの名前をプレイヤーの名前に設定
    plrStats.Name = player.Name
    -- "OwnsTycoon"という名前のObjectValueを作成して、プレイヤーが所有するタイクーン情報を保存するための場所を作成
    local isOwner = Instance.new("ObjectValue", plrStats)
    isOwner.Name = "OwnsTycoon"
end)

-- プレイヤーがゲームから退出したときに発火するイベント
game.Players.PlayerRemoving:connect(function(player)
    -- ServerStorageのPlayerMoneyからプレイヤーのステータスを探す
    local plrStats = game.ServerStorage.PlayerMoney:FindFirstChild(player.Name)
    -- もしステータスが存在すれば、それを破棄
    if plrStats ~= nil then
        plrStats:Destroy()
    end
    -- プレイヤーが所有しているタイクーンを探す(getPlrTycoonはこのコードには含まれていないが、おそらく他の部分で定義されている)
    local tycoon = getPlrTycoon(player)
    -- タイクーンが存在すれば
    if tycoon then
        -- タイクーンのバックアップを作成
        local backup = Tycoons[tycoon.Name]:Clone()
        -- オリジナルのタイクーンを破棄
        tycoon:Destroy()
        -- 少し待つ(このwaitはおそらく破棄処理が完了するのを待っている)
        wait()
        -- バックアップをTycoonsに再配置
        backup.Parent = script.Parent.Tycoons
    end
end)

DevProductHandler (script)

このスクリプトは、開発者がゲーム内で販売する商品を管理するためのものです。商品の価格設定や購入処理がここで行われます。

-- 設定モジュールを読み込む
local Settings = require(script.Parent.Settings)
-- Tycoonモデルを取得
local Tycoon = script.Parent.Tycoons:GetChildren()[1]
-- スクリプトの親をServerScriptServiceに設定
script.Parent = game.ServerScriptService
-- DevProductsを格納するテーブルを初期化
local DevProducts = {}
-- MarketplaceServiceを取得
local MarketplaceService = game:GetService('MarketplaceService')

-- Tycoon内のButtonsフォルダの各子要素(ボタン)をループで処理
for i,v in pairs(Tycoon:WaitForChild('Buttons'):GetChildren()) do
	-- DevProductという名前の子要素が存在するかチェック
	if v:FindFirstChild('DevProduct') then
		-- DevProductのValueが0より大きいかチェック
		if v.DevProduct.Value > 0 then
			-- DevProductのIDをキーとして、対応するボタンをテーブルに保存
			DevProducts[v.DevProduct.Value] = v
		end
	end
end

-- レシート処理関数を定義
MarketplaceService.ProcessReceipt = function(receiptInfo)
	-- 全プレイヤーをループで処理
	for i,plr in pairs(game.Players:GetPlayers()) do
		-- レシートのPlayerIdがプレイヤーのuserIdと一致するかチェック
		if plr.userId == receiptInfo.PlayerId then
			-- 購入されたDevProductがテーブルに存在するかチェック
			if DevProducts[receiptInfo.ProductId] then
				-- 対応するボタンを取得
				local obj = DevProducts[receiptInfo.ProductId]
				-- プレイヤーが所有するTycoonを取得
				local PlrT = game.ServerStorage.PlayerMoney:WaitForChild(plr.Name).OwnsTycoon
				-- Tycoonが存在するかチェック
				if PlrT.Value ~= nil then
					-- 購入処理(Create関数)を呼び出す
					local PlayerStats = game.ServerStorage.PlayerMoney:FindFirstChild(plr.Name)
					Create({[1] = 0,[2] = obj,[3] = PlayerStats}, PlrT.Value.BuyObject)
				end
			end
		end
	end
end

-- 購入処理関数(Create)を定義
function Create(tab, prnt)
	local x = Instance.new('Model')
	Instance.new('NumberValue',x).Value = tab[1]
	x.Value.Name = "Cost"
	Instance.new('ObjectValue',x).Value = tab[2]
	x.Value.Name = "Button"
	local Obj = Instance.new('ObjectValue',x)
	Obj.Name = "Stats"
	Obj.Value = tab[3]
	x.Parent = prnt
end

LinkedLeaderBoard (script)

このスクリプトは、ゲーム内のリーダーボードを管理しています。プレイヤーのスコアやランキング、その他のステータスを表示するためのものです。

設定の読み込み: Settings モジュールを読み込む

-- 設定モジュールを読み込む
local Settings = require(script.Parent.Settings)
-- スクリプトの親をServerScriptServiceに設定
script.Parent = game.ServerScriptService
-- FlagStandを格納するテーブルを初期化
stands = {}
-- CTFモードのフラグを初期化
CTF_mode = false

死亡イベント: プレイヤーが死んだときに、リーダーボードの「死亡数」を更新

-- プレイヤーが死んだときの処理
function onHumanoidDied(humanoid, player)
    -- プレイヤーのleaderstatsを探す
    local stats = player:findFirstChild("leaderstats")
    if stats ~= nil then
        -- 死亡数をインクリメント
        local deaths = stats:findFirstChild(Settings.LeaderboardSettings.DeathsName)
        if deaths then
            deaths.Value = deaths.Value + 1
        end
        -- KOsが有効な場合、キラーを探して処理
        if Settings.LeaderboardSettings.KOs then
            local killer = getKillerOfHumanoidIfStillInGame(humanoid)
            handleKillCount(humanoid, player)
        end
    end
end

プレイヤーのリスポーン: プレイヤーがリスポーンしたときに新しいHumanoidにイベントを接続

-- プレイヤーがリスポーンしたときに呼び出される関数
function onPlayerRespawn(property, player)
    -- 新しいHumanoidに接続する必要がある
    
    -- propertyが"Character"で、かつ、player.Characterがnilでない場合(つまり、プレイヤーが新しいキャラクターを持っている場合)
    if property == "Character" and player.Character ~= nil then
        -- プレイヤーのHumanoidを取得
        local humanoid = player.Character.Humanoid
        -- プレイヤーとHumanoidを変数に格納(おそらく後で使いやすくするため)
        local p = player
        local h = humanoid
        -- 設定でWOs(おそらく死亡数)が有効な場合
        if Settings.LeaderboardSettings.WOs then
            -- Humanoidが死んだときにonHumanoidDied関数を呼び出す
            humanoid.Died:connect(function() onHumanoidDied(h, p) end )
        end
    end
end


function getKillerOfHumanoidIfStillInGame(humanoid)
    -- このHumanoidを倒したプレイヤーオブジェクトを返す
    -- もしキラーがゲームにいない場合はnilを返す

    -- Humanoidに"creator"という名前のタグがついているか確認
    local tag = humanoid:findFirstChild("creator")

    -- タグが存在する場合
    if tag ~= nil then
        -- タグのValue(キラー)を取得
        local killer = tag.Value
        -- キラーがまだゲームにいる場合
        if killer.Parent ~= nil then
            return killer
        end
    end

    return nil
end

キルカウント: プレイヤーが他のプレイヤーを倒したときに、リーダーボードの「キル数」を更新。

-- キルカウントを処理する
function handleKillCount(humanoid, player)
    -- キラーを取得
    local killer = getKillerOfHumanoidIfStillInGame(humanoid)
    -- キラーが存在する場合
    if killer ~= nil then
        -- キラーのスタッツを取得
        local stats = killer:findFirstChild("leaderstats")
        -- スタッツが存在する場合
        if stats ~= nil then
            -- キル数を取得
            local kills = stats:findFirstChild(Settings.LeaderboardSettings.KillsNames)
            -- キル数が存在する場合
            if kills then
                -- キラーとプレイヤーが同一でない場合、キル数を+1
                if killer ~= player then
                    kills.Value = kills.Value + 1	
                else
                    -- キラーとプレイヤーが同一の場合、キル数を-1(自殺とみなす)
                    kills.Value = kills.Value - 1
                end
            else
                return
            end
        end
    end
end

フラグスタンドの探索: ゲーム内のすべてのフラグスタンドを探して、リスナーを設定

function findAllFlagStands(root)
    -- rootの子要素を取得
    local c = root:children()
    -- 子要素をループで処理
    for i=1,#c do
        -- 子要素が"Model"または"Part"の場合、再帰的に探索
        if (c[i].className == "Model" or c[i].className == "Part") then
            findAllFlagStands(c[i])
        end
        -- 子要素が"FlagStand"の場合、standsテーブルに追加
        if (c[i].className == "FlagStand") then
            table.insert(stands, c[i])
        end
    end
end

function hookUpListeners()
    -- standsテーブルに格納された各FlagStandに対してリスナーを設定
    for i=1,#stands do
        stands[i].FlagCaptured:connect(onCaptureScored)
    end
end

プレイヤーの入場: 新しいプレイヤーがゲームに参加したときに、リーダーボードの各種スタッツを初期化

function onPlayerEntered(newPlayer)
    -- CTFモード(Capture The Flag)が有効かどうかを確認
    if CTF_mode == true then
        -- leaderstatsという名前のIntValueを作成
        local stats = Instance.new("IntValue")
        stats.Name = "leaderstats"

        -- Capturesという名前のIntValueを作成して、初期値を0に設定
        local captures = Instance.new("IntValue")
        captures.Name = "Captures"
        captures.Value = 0

        -- Capturesをleaderstatsの子要素として設定
        captures.Parent = stats

        -- プレイヤーのキャラクターがロードされるまで待つ(注意: この方法は最適ではない)
        while true do
            if newPlayer.Character ~= nil then break end
            wait(5)
        end

        -- leaderstatsをプレイヤーの子要素として設定
        stats.Parent = newPlayer
    else
        -- 以下、CTFモードが無効の場合の処理(KOs, WOs, 通貨など)
        -- (省略している部分もあるが、基本的にはleaderstatsとその子要素を設定している)

        -- プレイヤーのキャラクターがロードされるまで待つ
        while true do
            if newPlayer.Character ~= nil then break end
            wait(5)
        end

        -- Humanoidが死んだときのイベントを設定
        local humanoid = newPlayer.Character.Humanoid
        humanoid.Died:connect(function() onHumanoidDied(humanoid, newPlayer) end )

        -- 新しいHumanoidが生成されたときのイベントを設定
        newPlayer.Changed:connect(function(property) onPlayerRespawn(property, newPlayer) end )

        -- leaderstatsをプレイヤーの子要素として設定
        stats.Parent = newPlayer
    end
end

キャプチャスコア: プレイヤーがフラグをキャプチャしたときに、リーダーボードの「キャプチャ数」を更新

function onCaptureScored(player)
    -- プレイヤーの子要素から"leaderstats"を探す
    local ls = player:findFirstChild("leaderstats")
    -- "leaderstats"がなければ、関数を終了
    if ls == nil then return end

    -- "leaderstats"の子要素から"Captures"を探す
    local caps = ls:findFirstChild("Captures")
    -- "Captures"がなければ、関数を終了
    if caps == nil then return end

    -- "Captures"の値を1増加させる
    caps.Value = caps.Value + 1
end

-- Workspace内のすべてのフラグスタンドを探して、それらをstands配列に追加する。
findAllFlagStands(game.Workspace)

-- stands配列に含まれる各フラグスタンドに、onCaptureScored関数を接続する。
hookUpListeners()

-- フラグスタンドが1つ以上存在する場合、CTFモード(Capture The Flagモード)を有効にする。
if (#stands > 0) then CTF_mode = true end

-- 新しいプレイヤーがゲームに参加したときに、onPlayerEntered関数を呼び出す。
game.Players.ChildAdded:connect(onPlayerEntered)

PurchaseHandler

概要
  • 設定のインポート: スクリプトの最初で、"Settings" モジュールスクリプトから設定をインポートしています。
  • チームカラーとサウンド: チームカラーを設定し、特定のイベントでサウンドを再生する関数があります。
  • パーツコレクター: ゲーム内のオブジェクト(パーツ)がコレクターに触れたときに、プレイヤーのお金(Currency)を増やします。
  • プレイヤーとコレクターのインタラクション: プレイヤーがコレクターに触れたときの処理があります。所有者かどうか、健康状態などに基づいて処理が行われます。
  • ボタンと購入: ゲーム内のボタンに触れたときの購入処理があります。価格、所有者、健康状態などに基づいて購入が行われます。
  • 購入関数: 購入処理を行う関数があります。コストを減らし、購入したオブジェクトをプレイヤーに与えます。
  • オブジェクトの作成: 新しいオブジェクト(モデル)を作成する関数があります。
  • 非効率なリスナー: スクリプトの最後には、非効率なChildAddedリスナーがあり、これは改善の余地があるとコメントされています。
コードセクションと解説
設定のインポート
-- 基本設定は "Settings" モジュールスクリプトにあります。
-- 変数の初期化
local Objects = {}  -- オブジェクトを格納するテーブル
local TeamColor = script.Parent.TeamColor.Value  -- チームの色
local Settings = require(script.Parent.Parent.Parent.Settings)  -- 設定モジュールの読み込み
local Money = script.Parent.CurrencyToCollect  -- 収集する通貨
local Debris = game:GetService('Debris')  -- Debrisサービスの取得
local Stealing = Settings.StealSettings  -- 盗む設定
local CanSteal = true  -- 盗むことができるかどうか(trueならできる)
チームカラーとサウンド
-- スポーン地点のチームカラーとブロックカラーを設定
script.Parent.Essentials.Spawn.TeamColor = TeamColor  -- スポーン地点のチームカラーを設定
script.Parent.Essentials.Spawn.BrickColor = TeamColor  -- スポーン地点のブロックカラーを設定


-- Sound関数: 指定されたパーツにサウンドを追加して再生する関数
function Sound(part, id)
    if part:FindFirstChild('Sound') then  -- もしパーツにすでに'Sound'があれば何もしない
        return
    else  -- そうでなければ新しい'Sound'を作成
        local Sound = Instance.new('Sound', part)  -- 'Sound'インスタンスを作成
        Sound.SoundId = "rbxassetid://" .. tostring(id)  -- サウンドIDを設定
        Sound:Play()  -- サウンドを再生
        delay(Sound.TimeLength, function()  -- サウンドの長さが終わったら
            Sound:Destroy()  -- 'Sound'を破棄
        end)
    end
end
パーツコレクター
-- パーツがコレクターに落ちたときの処理
for i, v in pairs(script.Parent.Essentials:GetChildren()) do  -- Essentialsの子要素をループ
    if v.Name == "PartCollector" then  -- 名前が"PartCollector"のものだけ処理
        v.Touched:connect(function(Part)  -- パーツが触れたときのイベント
            if Part:FindFirstChild('Cash') then  -- パーツに'Cash'があれば
                Money.Value = Money.Value + Part.Cash.Value  -- お金を加算
                Debris:AddItem(Part, 0.1)  -- パーツをDebrisに追加して0.1秒後に消去
            end
        end)
    end
end
プレイヤーとコレクターのインタラクション
-- Player Touched Collector processor
deb = false  -- デバウンス変数(連続実行を防ぐ)
script.Parent.Essentials.Giver.Touched:connect(function(hit)  -- Giverに何かが触れたときのイベント
    local player = game.Players:GetPlayerFromCharacter(hit.Parent)  -- 触れたオブジェクトの親からプレイヤーを取得
    if player ~= nil then  -- プレイヤーが存在する場合
        if script.Parent.Owner.Value == player then  -- オーナーが触れた場合
            if hit.Parent:FindFirstChild("Humanoid") then  -- Humanoidが存在する場合
                if hit.Parent.Humanoid.Health > 0 then  -- プレイヤーが生きている場合
                    if deb == false then  -- デバウンスがfalseの場合
                        deb = true  -- デバウンスをtrueに設定
                        script.Parent.Essentials.Giver.BrickColor = BrickColor.new("Bright red")  -- Giverの色を赤に変更
                        local Stats = game.ServerStorage.PlayerMoney:FindFirstChild(player.Name)  -- プレイヤーのお金の情報を取得
                        if Stats ~= nil then  -- お金の情報が存在する場合
                            Sound(script.Parent.Essentials, Settings.Sounds.Collect)  -- コレクト音を再生
                            Stats.Value = Stats.Value + Money.Value  -- お金を加算
                            Money.Value = 0  -- Moneyをリセット
                            wait(1)  -- 1秒待つ
                            script.Parent.Essentials.Giver.BrickColor = BrickColor.new("Sea green")  -- Giverの色を緑に戻す
                            deb = false  -- デバウンスをfalseに戻す
                        end
                    end
                end
            end
        elseif Stealing.Stealing then  -- オーナーでなく、Stealingが有効な場合
            if CanSteal == true then  -- 盗むことができる場合
                CanSteal = false  -- CanStealをfalseに設定
                delay(Stealing.PlayerProtection, function()  -- PlayerProtection時間後に
                    CanSteal = true  -- CanStealをtrueに戻す
                end)
                if hit.Parent:FindFirstChild("Humanoid") then  -- Humanoidが存在する場合
                    if hit.Parent.Humanoid.Health > 0 then  -- プレイヤーが生きている場合
                        local Stats = game.ServerStorage.PlayerMoney:FindFirstChild(player.Name)  -- プレイヤーのお金の情報を取得
                        if Stats ~= nil then  -- お金の情報が存在する場合
                            local Difference = math.floor(Money.Value * Stealing.StealPrecent)  -- 盗むお金を計算
                            Sound(script.Parent.Essentials, Settings.Sounds.Collect)  -- コレクト音を再生
                            Stats.Value = Stats.Value + Difference  -- お金を加算
                            Money.Value = Money.Value - Difference  -- Moneyから減算
                        end
                    end
                end
            else
                Sound(script.Parent.Essentials, Settings.Sounds.Error)  -- エラー音を再生
            end
        end
    end
end)

ボタンと購入

このコードは、ゲーム内でプレイヤーが特定のボタン("Head"と呼ばれる部分)に触れたときに発生するイベントを処理しています。

依存オブジェクト("Dependency")が存在する場合、そのオブジェクトが存在するまでボタンは非表示となります。
プレイヤーがボタンに触れた場合、そのプレイヤーがオーナーであれば、購入処理が行われます。

script.Parent:WaitForChild("Buttons")  -- "Buttons"が存在するまで待つ
for i,v in pairs(script.Parent.Buttons:GetChildren()) do  -- "Buttons"の子要素をループで処理
    spawn(function()  -- 新しいスレッドで処理を開始
        if v:FindFirstChild("Head") then  -- "Head"が存在する場合
            
            local ThingMade = script.Parent.Purchases:WaitForChild(v.Object.Value)  -- 対応するオブジェクトを取得
            if ThingMade ~= nil then  -- オブジェクトが存在する場合
                Objects[ThingMade.Name] = ThingMade:Clone()  -- オブジェクトをクローンして保存
                ThingMade:Destroy()  -- 元のオブジェクトを削除
            else  -- オブジェクトが存在しない場合
                -- ボタンを無効にしてエラーメッセージを出力
                v.Head.CanCollide = false
                v.Head.Transparency = 1
                error('Object missing for button: '..v.Name..', button has been removed')
            end
            
            if v:FindFirstChild("Dependency") then  -- 依存オブジェクトが存在する場合
                v.Head.CanCollide = false
                v.Head.Transparency = 1
                coroutine.resume(coroutine.create(function()  -- コルーチンで非同期処理
                    if script.Parent.PurchasedObjects:WaitForChild(v.Dependency.Value) then  -- 依存オブジェクトが存在する場合
                        if Settings['ButtonsFadeIn'] then  -- フェードインが有効な場合
                            for i=1,20 do
                                wait(Settings['FadeInTime']/20)
                                v.Head.Transparency = v.Head.Transparency - 0.05
                            end
                        end
                        v.Head.CanCollide = true
                        v.Head.Transparency = 0
                    end
                end))
            end
            
            -- プレイヤーがボタン(Head)に触れた場合の処理を接続
		v.Head.Touched:connect(function(hit)
		    -- 触れたオブジェクトの親からプレイヤーを取得
		    local player = game.Players:GetPlayerFromCharacter(hit.Parent)

		    -- ボタンが衝突可能(CanCollide)である場合
		    if v.Head.CanCollide == true then
			-- プレイヤーが存在する場合
			if player ~= nil then
			    -- プレイヤーがこのオブジェクト(script.Parent)の所有者である場合
			    if script.Parent.Owner.Value == player then
				-- 触れたオブジェクトがHumanoid(キャラクター)である場合
				if hit.Parent:FindFirstChild("Humanoid") then
				    -- キャラクターの体力が0より大きい場合
				    if hit.Parent.Humanoid.Health > 0 then
					-- プレイヤーのお金(PlayerMoney)を取得
					local PlayerStats = game.ServerStorage.PlayerMoney:FindFirstChild(player.Name)

					-- プレイヤーのお金が存在する場合
					if PlayerStats ~= nil then
					    -- ゲームパスが設定されている場合
					    if (v:FindFirstChild('Gamepass')) and (v.Gamepass.Value >= 1) then
						-- プレイヤーがゲームパスを所有している場合
						if game:GetService("MarketplaceService"):UserOwnsGamePassAsync(player.userId, v.Gamepass.Value) then
						    Purchase({[1] = v.Price.Value,[2] = v,[3] = PlayerStats})
						else
						    -- ゲームパス購入画面を表示
						    game:GetService("MarketplaceService"):PromptGamePassPurchase(player, v.Gamepass.Value)
						end
					    -- 開発者製品が設定されている場合
					    elseif (v:FindFirstChild('DevProduct')) and (v.DevProduct.Value >= 1) then
						-- 開発者製品購入画面を表示
						game:GetService('MarketplaceService'):PromptProductPurchase(player,v.DevProduct.Value)
					    -- プレイヤーがアイテムを購入できる場合
					    elseif PlayerStats.Value >= v.Price.Value then
						Purchase({[1] = v.Price.Value,[2] = v,[3] = PlayerStats})
						Sound(v, Settings.Sounds.Purchase)
					    else
						-- 購入できない場合のエラーサウンド
						Sound(v, Settings.Sounds.ErrorBuy)
					    end
					end
				    end
				end
			    end
			end
		    end
		end)

        end
    end)
end
購入関数

このPurchase関数は、プレイヤーがアイテムを購入したときに呼び出される関数です。

まず、プレイヤーのお金(stats.Value)からアイテムのコスト(cost)を引きます。
次に、購入したアイテムをPurchasedObjectsフォルダに移動します。
最後に、設定によってはボタン(item.Head)がフェードアウトします。これは視覚的なエフェクトで、アイテムが購入されたことをプレイヤーに知らせます。

-- Purchase関数:アイテムを購入する処理
function Purchase(tbl)
    -- 引数から各種情報を取得
    local cost = tbl[1]  -- アイテムのコスト
    local item = tbl[2]  -- 購入するアイテム
    local stats = tbl[3]  -- プレイヤーのお金(またはステータス)

    -- プレイヤーのお金を減らす
    stats.Value = stats.Value - cost

    -- 購入したアイテムをPurchasedObjectsに移動
    Objects[item.Object.Value].Parent = script.Parent.PurchasedObjects

    -- ボタンのフェードアウト設定が有効な場合
    if Settings['ButtonsFadeOut'] then
        -- 衝突を無効にする
        item.Head.CanCollide = false

        -- コルーチンでフェードアウト処理
        coroutine.resume(coroutine.create(function()
            for i=1,20 do
                wait(Settings['FadeOutTime']/20)
                item.Head.Transparency = item.Head.Transparency + 0.05
            end
        end))
    else
        -- フェードアウト設定が無効な場合、直接透明にする
        item.Head.CanCollide = false
        item.Head.Transparency = 1
    end
end
オブジェクトの作成

このCreate関数は、新しい購入可能なオブジェクト(Model)を作成して、それをBuyObjectの子として追加します。

まず、新しいModelインスタンスを作成します。
次に、そのModelにNumberValueとObjectValueを追加します。これらはそれぞれコストとボタンに関する情報を保持します。
最後に、新しいObjectValueを作成して、その名前を"Stats"に設定します。これはプレイヤーのステータスに関する情報を保持します。

-- Create関数:新しい購入可能なオブジェクトを作成する
function Create(tab)
    -- 新しいModelインスタンスを作成
    local x = Instance.new('Model')

    -- NumberValueを作成して、そのValueにtab[1](コスト)を設定
    Instance.new('NumberValue', x).Value = tab[1]
    -- そのNumberValueの名前を"Cost"に設定
    x.Value.Name = "Cost"

    -- ObjectValueを作成して、そのValueにtab[2](ボタン)を設定
    Instance.new('ObjectValue', x).Value = tab[2]
    -- そのObjectValueの名前を"Button"に設定
    x.Value.Name = "Button"

    -- 新しいObjectValue(名前は"Stats")を作成して、そのValueにtab[3](ステータス)を設定
    local Obj = Instance.new('ObjectValue', x)
    Obj.Name = "Stats"
    Obj.Value = tab[3]

    -- 作成したModelをBuyObjectの子として設定
    x.Parent = script.Parent.BuyObject
end

GateControl

概要
  • プレイヤーの確認: スクリプトはプレイヤーがゲート(Head)に触れたときに発火します。そのプレイヤーがゲーム内に存在するかどうかを確認します。
  • プレイヤーのステータス確認: game.ServerStorage.PlayerMoney からプレイヤーのステータスを探します。
  • 所有状態の確認: プレイヤーがすでに「Tycoon」を所有しているかどうかを確認します。
  • 所有者の設定: もし「Tycoon」がまだ誰も所有していない場合、そのプレイヤーを新しい所有者として設定します。
  • ゲートの属性変更: 所有者が設定された後、ゲート(Head)の透明度と衝突属性を変更します。
  • チームカラーの設定: 新しい所有者のチームカラーを「Tycoon」のチームカラーに設定します。
コードセクションと解説
-- プレイヤーがゲート(Head)に触れたときにこの関数が呼び出されます
script.Parent.Head.Touched:connect(function(hit)
    -- 触れたキャラクターからプレイヤーを取得
    local player = game.Players:GetPlayerFromCharacter(hit.Parent)
    
    -- プレイヤーが存在する場合
    if player ~= nil then
        -- プレイヤーのステータス(PlayerMoney)をServerStorageから取得
        local PlayerStats = game.ServerStorage.PlayerMoney:FindFirstChild(player.Name)
        
        -- プレイヤーのステータスが存在する場合
        if PlayerStats ~= nil then
            -- プレイヤーが「Tycoon」を所有しているかどうかの情報を取得
            local ownstycoon = PlayerStats:FindFirstChild("OwnsTycoon")
            
            -- "OwnsTycoon" が存在する場合
            if ownstycoon ~= nil then
                -- まだ「Tycoon」を所有していない場合
                if ownstycoon.Value == nil then
                    -- まだ誰も「Tycoon」を所有していない場合
                    if script.Parent.Parent.Parent.Owner.Value == nil then
                        -- 触れたキャラクターが生きている場合
                        if hit.Parent:FindFirstChild("Humanoid") then
                            if hit.Parent.Humanoid.Health > 0 then
                                -- 新しい所有者としてプレイヤーを設定
                                script.Parent.Parent.Parent.Owner.Value = player
                                ownstycoon.Value = script.Parent.Parent.Parent
                                
                                -- ゲート(Head)の名前と透明度、衝突属性を更新
                                script.Parent.Name = player.Name.."'s Tycoon"
                                script.Parent.Head.Transparency = 0.6
                                script.Parent.Head.CanCollide = false
                                
                                -- 新しい所有者のチームカラーを設定
                                player.TeamColor = script.Parent.Parent.Parent.TeamColor.Value
                            end
                        end
                    end
                end
            end
        end
    end
end)

まとめ

Zednov's Tycoon Kitの基本的な使い方と中身の構成について調べたことで、Tycoon作成のノウハウが少しわかってきました。
このKitではデータのクラウド保存はしてなさそうですので、よくあるデータを自動保存して、次回アクセス時にロードされるような仕組みは別途実装が必要になりそうです。

また、Core_handlerもWorkspaceに配置するモデルとなっているため、チート対策など考慮するとServerScriptService側へ配置できるように修正が必要かもしれません。

色々細かいところはありますが、大枠を理解する上でシンプルですごく良い教材でした。
ディレクトリ構成も参考にしつつ、自分のゲームに上手く取り入れながら開発を進めたいと思います。

長々とお付き合い頂きありがとうございます。

Discussion