Skip to content

Instantly share code, notes, and snippets.

@gitcrtn
Last active August 2, 2025 05:33
Show Gist options
  • Select an option

  • Save gitcrtn/5e2e7f4d64bfa6ca26b657fb352de66b to your computer and use it in GitHub Desktop.

Select an option

Save gitcrtn/5e2e7f4d64bfa6ca26b657fb352de66b to your computer and use it in GitHub Desktop.
VRC Portal Auto Placer for Unity
How to use:
1. Open your "Favorite Worlds" page from VRChat Site on Chrome/Edge browser.
2. Switch to List View.
3. Run DumpFavWorldsSnippet.js as snippet from devtools.
4. Copy json from console of devtools.
5. Put PortalAutoPlacer.cs in Assets/Editor/ of your unity project.
6. Run "Tools/Portal Auto Placer" from Unity.
7. Paste json to textbox of Portal Auto Placer.
8. Click "Load JSON Text" button.
9. Click "Create Portals" button.
// VRChat Favorite Worlds ID Extractor
// Chrome DevTools Snippetで実行してください
(function() {
console.log('VRChat Favorite Worlds ID Extractor - 開始');
// 詳細なDOM解析を行う関数
function deepScanForWorldIds() {
console.log('詳細スキャンを開始...');
const worldIds = [];
// 全てのテキストノードとdata属性をスキャン
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_ELEMENT,
null,
false
);
let node;
while (node = walker.nextNode()) {
// data属性をチェック
for (const attr of node.attributes || []) {
if (attr.value.includes('wrld_')) {
const matches = attr.value.match(/wrld_[a-zA-Z0-9_-]+/g);
if (matches) {
matches.forEach(match => {
if (!worldIds.includes(match)) {
worldIds.push(match);
console.log(`詳細スキャンでWorld ID発見: ${match} (属性: ${attr.name})`);
}
});
}
}
}
// テキストコンテンツをチェック
const text = node.textContent || '';
const matches = text.match(/wrld_[a-zA-Z0-9_-]+/g);
if (matches) {
matches.forEach(match => {
if (!worldIds.includes(match)) {
worldIds.push(match);
console.log(`詳細スキャンでWorld ID発見: ${match} (テキスト)`);
}
});
}
}
return worldIds;
}
// ReactのFiberノードから情報を抽出する関数
function scanReactFiber() {
console.log('React Fiberスキャンを開始...');
const worldIds = [];
// React Fiberルートを探す
const reactRoots = document.querySelectorAll('[data-reactroot], #root, .react-root');
reactRoots.forEach(root => {
const fiberKey = Object.keys(root).find(key => key.startsWith('__reactInternalInstance') || key.startsWith('_reactInternalFiber'));
if (fiberKey) {
console.log('React Fiberが見つかりました');
// Fiberツリーを再帰的に探索(深すぎないように制限)
function traverseFiber(fiber, depth = 0) {
if (!fiber || depth > 10) return;
if (fiber.memoizedProps) {
const props = JSON.stringify(fiber.memoizedProps);
const matches = props.match(/wrld_[a-zA-Z0-9_-]+/g);
if (matches) {
matches.forEach(match => {
if (!worldIds.includes(match)) {
worldIds.push(match);
console.log(`React FiberでWorld ID発見: ${match}`);
}
});
}
}
// 子要素を探索
if (fiber.child) traverseFiber(fiber.child, depth + 1);
if (fiber.sibling) traverseFiber(fiber.sibling, depth + 1);
}
traverseFiber(root[fiberKey]);
}
});
return worldIds;
}
// ネットワークリクエストを監視する関数
function monitorNetworkRequests() {
console.log('ネットワーク監視を開始(5秒間)...');
const originalFetch = window.fetch;
const foundIds = [];
window.fetch = function(...args) {
const url = args[0];
return originalFetch.apply(this, args).then(response => {
if (url.includes('world') || url.includes('favorite')) {
response.clone().text().then(text => {
const matches = text.match(/wrld_[a-zA-Z0-9_-]+/g);
if (matches) {
matches.forEach(match => {
if (!foundIds.includes(match)) {
foundIds.push(match);
console.log(`ネットワークリクエストでWorld ID発見: ${match}`);
}
});
}
}).catch(() => {});
}
return response;
});
};
// 5秒後に元のfetch関数を復元
setTimeout(() => {
window.fetch = originalFetch;
console.log('ネットワーク監視終了');
}, 5000);
return foundIds;
}
// メイン実行
console.log('=== World IDとワールド名を収集中 ===');
// ワールド名を取得する関数
function getWorldName(worldId, element) {
let worldName = 'Unknown World';
if (element) {
// まず、要素の周辺でh4要素を探す
const h4Element = element.querySelector('h4') ||
element.closest('.world-item, .favorite-world, [class*="world"]')?.querySelector('h4') ||
element.parentElement?.querySelector('h4');
if (h4Element) {
// h4のtitle属性を最優先
if (h4Element.title && h4Element.title.trim()) {
worldName = h4Element.title.trim();
}
// h4のテキストコンテンツ
else if (h4Element.textContent && h4Element.textContent.trim()) {
worldName = h4Element.textContent.trim();
}
}
// h4が見つからない場合は他の方法を試す
if (worldName === 'Unknown World') {
// 要素自体のtitle属性
if (element.title && element.title.trim() && !element.title.includes('World Image')) {
worldName = element.title.trim();
}
// aria-label属性
else if (element.getAttribute('aria-label') && !element.getAttribute('aria-label').includes('World Image')) {
worldName = element.getAttribute('aria-label');
}
// 近くのテキスト要素を探す
else {
const textElements = element.querySelectorAll('h1, h2, h3, h4, h5, h6, .title, [class*="title"], [class*="name"]');
for (const textEl of textElements) {
const text = textEl.textContent?.trim();
if (text && text !== worldId && !text.includes('wrld_') && !text.includes('World Image')) {
worldName = text;
break;
}
}
}
}
// まだ見つからない場合は親要素を遡って探す
if (worldName === 'Unknown World') {
let parent = element.parentElement;
let attempts = 0;
while (parent && attempts < 3) {
// 親要素内でh4を探す
const h4InParent = parent.querySelector('h4');
if (h4InParent && h4InParent.title) {
worldName = h4InParent.title.trim();
break;
} else if (h4InParent && h4InParent.textContent) {
const text = h4InParent.textContent.trim();
if (text && !text.includes('World Image')) {
worldName = text;
break;
}
}
parent = parent.parentElement;
attempts++;
}
}
}
// World IDが含まれていたら除去
worldName = worldName.replace(/wrld_[a-zA-Z0-9_-]+/g, '').trim();
// 「World Image」は除外
if (worldName.includes('World Image') || worldName === 'World Image') {
worldName = 'Unknown World';
}
// 長すぎる場合は短縮
if (worldName.length > 100) {
worldName = worldName.substring(0, 97) + '...';
}
return worldName || 'Unknown World';
}
// World IDと名前のペアを収集する関数
function collectWorldData() {
const worldData = [];
const processedIds = new Set();
// 各セレクターで要素とWorld IDの関連を保持
const selectors = [
'a[href*="/home/world/"]',
'a[href*="/world/"]',
'[data-world-id]',
'.world-item',
'.favorite-world',
'div[id*="world"]',
'div[class*="world"]',
'h4[title]', // h4要素も直接検索
'[class*="css-"] h4', // CSSクラスを持つh4要素
];
selectors.forEach(selector => {
try {
const elements = document.querySelectorAll(selector);
elements.forEach(element => {
let worldId = null;
// h4要素の場合は周辺でWorld IDを探す
if (element.tagName === 'H4') {
// h4要素の親や兄弟要素でWorld IDを探す
let searchElement = element.parentElement;
let attempts = 0;
while (searchElement && attempts < 3) {
// href属性を持つリンクを探す
const linkElement = searchElement.querySelector('a[href*="/world/"]');
if (linkElement && linkElement.href) {
const match = linkElement.href.match(/\/world\/([a-zA-Z0-9_-]+)/);
if (match) {
worldId = match[1];
break;
}
}
// data属性を探す
const dataElement = searchElement.querySelector('[data-world-id]');
if (dataElement) {
worldId = dataElement.dataset.worldId;
break;
}
// テキストコンテンツでWorld IDを探す
const text = searchElement.textContent || '';
const match = text.match(/wrld_[a-zA-Z0-9_-]+/);
if (match) {
worldId = match[0];
break;
}
searchElement = searchElement.parentElement;
attempts++;
}
}
// 通常の要素の場合
else {
// href属性からworld IDを抽出
if (element.href) {
const match = element.href.match(/\/world\/([a-zA-Z0-9_-]+)/);
if (match) {
worldId = match[1];
}
}
// data属性からworld IDを抽出
if (!worldId && element.dataset) {
worldId = element.dataset.worldId ||
element.dataset.id ||
element.dataset.worldid;
}
// テキストコンテンツからworld IDを抽出
if (!worldId) {
const text = element.textContent || element.innerText || '';
const match = text.match(/wrld_[a-zA-Z0-9_-]+/);
if (match) {
worldId = match[0];
}
}
}
if (worldId && !processedIds.has(worldId)) {
processedIds.add(worldId);
const worldName = getWorldName(worldId, element);
worldData.push({ name: worldName, id: worldId });
console.log(`World発見: ${worldName} (${worldId})`);
}
});
} catch (error) {
console.warn(`セレクター "${selector}" でエラー:`, error);
}
});
// 詳細スキャンで見つかったIDの名前も探す
const deepScanIds = deepScanForWorldIds();
deepScanIds.forEach(worldId => {
if (!processedIds.has(worldId)) {
processedIds.add(worldId);
// IDのみの場合は要素を再検索
const elements = document.querySelectorAll(`[href*="${worldId}"], [data-world-id="${worldId}"]`);
const worldName = elements.length > 0 ? getWorldName(worldId, elements[0]) : 'Unknown World';
worldData.push({ name: worldName, id: worldId });
}
});
return worldData;
}
// 結果出力
setTimeout(() => {
console.log('\n=== World IDと名前を収集中 ===');
const worldData = collectWorldData();
console.log('\n=== 結果 ===');
console.log(`発見されたワールド数: ${worldData.length}`);
console.log('\nワールドリスト(ワールド名: worldid):');
if (worldData.length > 0) {
worldData.forEach((world, index) => {
console.log(`${world.name}: ${world.id}`);
});
console.log('\n=== コピー用フォーマット ===');
const formattedList = worldData.map(world => `${world.name}: ${world.id}`).join('\n');
console.log(formattedList);
// JSON形式でも出力
console.log('\n=== JSON形式 ===');
console.log(JSON.stringify(worldData, null, 2));
} else {
console.log('World IDが見つかりませんでした。');
console.log('以下を確認してください:');
console.log('1. VRChatのFavorite Worldsページにいるか');
console.log('2. ページが完全に読み込まれているか');
console.log('3. ログインしているか');
console.log('\nページの構造を確認するため、以下を実行してみてください:');
console.log('document.querySelectorAll("*").length'); // 総要素数
console.log('document.body.innerHTML.includes("world")'); // worldという文字列があるか
}
}, 6000); // ネットワーク監視終了後に結果表示
})();
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using VRC.SDK3.Components;
namespace VRChatTools
{
[System.Serializable]
public class WorldData
{
public string name;
public string id;
}
[System.Serializable]
public class WorldDataList
{
public List<WorldData> worlds;
}
public class VRCPortalAutoplacer : EditorWindow
{
private string jsonTextInput = "";
private WorldDataList worldDataList;
private Vector2 scrollPosition;
private Vector2 jsonScrollPosition;
private bool showPreview = false;
private bool reverseSort = false; // JSONデータをリバースソートするかどうか
// 配置設定
private Vector3 startPosition = Vector3.zero;
private Vector3 spacing = new Vector3(3f, 0f, 3f); // Z成分を3に修正
private int portalsPerRow = 5;
private int portalsPerColumn = 5; // 縦方向のポータル数
private string parentObjectName = "Portal Container";
private bool useGridLayout = true;
private bool useDualWallLayout = true; // 対面配置オプション
private float wallDistance = 6f; // 壁間の距離
private float wallThickness = 0.5f; // 壁の厚さ
private bool createWalls = true; // 壁オブジェクトを作成するかどうか
private Color wallColor = Color.gray; // 壁の色
// プレファブ設定
private GameObject portalPrefab;
private bool autoFindPortalPrefab = true;
[MenuItem("Tools/Portal Auto Placer")]
public static void ShowWindow()
{
VRCPortalAutoplacer window = GetWindow<VRCPortalAutoplacer>("VRC Portal Auto Placer");
window.minSize = new Vector2(400, 600);
window.Show();
}
private void OnEnable()
{
if (autoFindPortalPrefab)
{
FindPortalPrefab();
}
}
private void OnGUI()
{
GUILayout.Label("VRChat Portal Auto Placer", EditorStyles.boldLabel);
GUILayout.Space(10);
// JSON読み込みセクション
EditorGUILayout.LabelField("JSON Data Input", EditorStyles.boldLabel);
// JSONソートオプション
reverseSort = EditorGUILayout.Toggle("Reverse Sort JSON Data", reverseSort);
// テキスト入力モード
EditorGUILayout.LabelField("Paste JSON Data Here:");
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Clear", GUILayout.Width(60)))
{
jsonTextInput = "";
GUI.FocusControl(null);
}
if (GUILayout.Button("Paste", GUILayout.Width(60)))
{
jsonTextInput = EditorGUIUtility.systemCopyBuffer;
GUI.FocusControl(null);
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Load JSON Text", GUILayout.Width(120)))
{
LoadWorldDataFromText();
}
EditorGUILayout.EndHorizontal();
// スクロール可能なテキストエリア
jsonScrollPosition = EditorGUILayout.BeginScrollView(jsonScrollPosition, GUILayout.Height(150));
EditorGUILayout.LabelField("JSON Content:", EditorStyles.miniLabel);
jsonTextInput = EditorGUILayout.TextArea(jsonTextInput, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
// JSONが入力されている場合は自動読み込みボタンを表示
if (!string.IsNullOrEmpty(jsonTextInput) && jsonTextInput.Trim().Length > 10)
{
GUI.backgroundColor = Color.cyan;
if (GUILayout.Button("Auto Load JSON (Text Changed)", GUILayout.Height(25)))
{
LoadWorldDataFromText();
}
GUI.backgroundColor = Color.white;
}
GUILayout.Space(10);
// プレファブ設定セクション
EditorGUILayout.LabelField("Portal Prefab Settings", EditorStyles.boldLabel);
autoFindPortalPrefab = EditorGUILayout.Toggle("Auto Find Portal Prefab", autoFindPortalPrefab);
if (!autoFindPortalPrefab)
{
portalPrefab = (GameObject)EditorGUILayout.ObjectField("Portal Prefab", portalPrefab, typeof(GameObject), false);
}
else
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.ObjectField("Found Portal Prefab", portalPrefab, typeof(GameObject), false);
if (GUILayout.Button("Refresh", GUILayout.Width(80)))
{
FindPortalPrefab();
}
EditorGUILayout.EndHorizontal();
}
GUILayout.Space(10);
// 配置設定セクション
EditorGUILayout.LabelField("Placement Settings", EditorStyles.boldLabel);
parentObjectName = EditorGUILayout.TextField("Parent Object Name", parentObjectName);
EditorGUILayout.HelpBox("All portals will be grouped under a single Empty GameObject.", MessageType.Info);
useGridLayout = EditorGUILayout.Toggle("Use Grid Layout", useGridLayout);
if (useGridLayout)
{
portalsPerColumn = EditorGUILayout.IntField("Portals Per Column", portalsPerColumn);
spacing = EditorGUILayout.Vector3Field("Spacing", spacing);
GUILayout.Space(5);
useDualWallLayout = EditorGUILayout.Toggle("Dual Wall Layout", useDualWallLayout);
if (useDualWallLayout)
{
EditorGUILayout.HelpBox("Portals will be arranged in columns facing each other through walls.\nLayout: Front] | [Back Front] | [Back", MessageType.Info);
wallDistance = EditorGUILayout.FloatField("Wall Distance", wallDistance);
wallThickness = EditorGUILayout.FloatField("Wall Thickness", wallThickness);
wallColor = EditorGUILayout.ColorField("Wall Color", wallColor);
createWalls = EditorGUILayout.Toggle("Create Wall Objects", createWalls);
}
}
startPosition = EditorGUILayout.Vector3Field("Start Position", startPosition);
GUILayout.Space(10);
// データプレビューセクション
if (worldDataList != null && worldDataList.worlds != null)
{
EditorGUILayout.LabelField($"Loaded Worlds: {worldDataList.worlds.Count}", EditorStyles.boldLabel);
showPreview = EditorGUILayout.Foldout(showPreview, "World Data Preview");
if (showPreview)
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(200));
for (int i = 0; i < worldDataList.worlds.Count; i++)
{
var world = worldDataList.worlds[i];
EditorGUILayout.LabelField($"{i + 1}. {world.name}", EditorStyles.wordWrappedLabel);
EditorGUILayout.LabelField($" ID: {world.id}", EditorStyles.miniLabel);
GUILayout.Space(2);
}
EditorGUILayout.EndScrollView();
}
GUILayout.Space(10);
// 実行ボタン
GUI.backgroundColor = Color.green;
if (GUILayout.Button("Create Portals", GUILayout.Height(40)))
{
CreatePortals();
}
GUI.backgroundColor = Color.white;
}
else
{
EditorGUILayout.HelpBox("No world data loaded. Please paste JSON data and click 'Load JSON Text'.", MessageType.Info);
}
}
private void FindPortalPrefab()
{
// VRCPortalMarkerプレファブを自動検索
string[] guids = AssetDatabase.FindAssets("t:Prefab");
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (prefab != null && prefab.GetComponent<VRCPortalMarker>() != null)
{
portalPrefab = prefab;
Debug.Log($"Portal prefab found: {path}");
return;
}
}
// VRC SDK内のプレファブも検索
string[] vrcGuids = AssetDatabase.FindAssets("VRCPortalMarker t:Prefab");
if (vrcGuids.Length > 0)
{
string path = AssetDatabase.GUIDToAssetPath(vrcGuids[0]);
portalPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
Debug.Log($"VRC Portal prefab found: {path}");
}
else
{
Debug.LogWarning("VRCPortalMarker prefab not found. Please assign manually.");
}
}
private void LoadWorldDataFromText()
{
if (string.IsNullOrEmpty(jsonTextInput?.Trim()))
{
EditorUtility.DisplayDialog("Error", "JSON text input is empty.", "OK");
return;
}
try
{
ProcessJsonData(jsonTextInput);
Debug.Log($"Loaded {worldDataList.worlds.Count} worlds from text input.");
}
catch (System.Exception e)
{
EditorUtility.DisplayDialog("Error", $"Failed to parse JSON text: {e.Message}", "OK");
Debug.LogError($"JSON Text Parse Error: {e.Message}");
}
}
private void ProcessJsonData(string jsonContent)
{
// 配列形式のJSONかどうかを判定
if (jsonContent.TrimStart().StartsWith("["))
{
// 配列形式の場合はオブジェクトでラップ
jsonContent = "{\"worlds\":" + jsonContent + "}";
}
else if (!jsonContent.Contains("\"worlds\""))
{
// オブジェクト形式だが"worlds"キーがない場合の処理
throw new System.Exception("JSON format not supported. Expected array of {name, id} objects or object with 'worlds' key.");
}
worldDataList = JsonUtility.FromJson<WorldDataList>(jsonContent);
if (worldDataList?.worlds == null || worldDataList.worlds.Count == 0)
{
throw new System.Exception("No valid world data found in JSON.");
}
// リバースソートオプションが有効な場合、リストを逆順にする
if (reverseSort)
{
worldDataList.worlds.Reverse();
}
Repaint();
}
private void CreatePortals()
{
if (portalPrefab == null)
{
EditorUtility.DisplayDialog("Error", "Portal prefab is not assigned.", "OK");
return;
}
if (worldDataList?.worlds == null || worldDataList.worlds.Count == 0)
{
EditorUtility.DisplayDialog("Error", "No world data loaded.", "OK");
return;
}
// 必ず親オブジェクトを作成
GameObject parentObject = new GameObject(parentObjectName);
parentObject.transform.position = startPosition;
Undo.RegisterCreatedObjectUndo(parentObject, "Create Portal Container");
if (useDualWallLayout && useGridLayout)
{
CreateDualWallLayout(parentObject);
}
else
{
CreateStandardLayout(parentObject);
}
// 親オブジェクトを選択状態にする
Selection.activeGameObject = parentObject;
}
private void CreateStandardLayout(GameObject parentObject)
{
int portalCount = 0;
foreach (var world in worldDataList.worlds)
{
Vector3 position = CalculateStandardPosition(portalCount);
GameObject portal = (GameObject)PrefabUtility.InstantiatePrefab(portalPrefab);
if (portal != null)
{
portal.transform.SetParent(parentObject.transform);
portal.transform.localPosition = position - startPosition;
portal.transform.localRotation = Quaternion.identity;
VRCPortalMarker marker = portal.GetComponent<VRCPortalMarker>();
if (marker != null)
{
marker.roomId = world.id;
}
portal.name = $"Portal {portalCount + 1} - {world.name}";
Undo.RegisterCreatedObjectUndo(portal, "Create Portal");
}
portalCount++;
}
EditorUtility.DisplayDialog("Success", $"Created {portalCount} portals under '{parentObjectName}' successfully!", "OK");
}
private void CreateDualWallLayout(GameObject parentObject)
{
int totalPortals = worldDataList.worlds.Count;
int portalsCreated = 0;
int wallsCreated = 0;
// 必要な列数を計算(2列で1組)
int totalGroups = Mathf.CeilToInt((float)totalPortals / (portalsPerColumn * 2));
for (int group = 0; group < totalGroups; group++)
{
float groupBaseX = group * wallDistance;
// 表側(左側)の列 - 壁の左側に配置
for (int row = 0; row < portalsPerColumn; row++)
{
int portalIndex = group * portalsPerColumn * 2 + row;
if (portalIndex >= totalPortals) break;
var world = worldDataList.worlds[portalIndex];
Vector3 position = new Vector3(
groupBaseX - wallThickness / 2f - 0.1f, // 壁の左側
row * spacing.y,
row * spacing.z // Z座標を3ずつずらす
);
GameObject portal = (GameObject)PrefabUtility.InstantiatePrefab(portalPrefab);
if (portal != null)
{
portal.transform.SetParent(parentObject.transform);
portal.transform.localPosition = position;
portal.transform.localRotation = Quaternion.Euler(0, -90, 0); // 右向き(壁に向かって)
VRCPortalMarker marker = portal.GetComponent<VRCPortalMarker>();
if (marker != null)
{
marker.roomId = world.id;
}
portal.name = $"Portal {portalIndex + 1} - {world.name}";
Undo.RegisterCreatedObjectUndo(portal, "Create Portal");
portalsCreated++;
}
}
// 裏側(右側)の列 - 壁の右側に配置
for (int row = 0; row < portalsPerColumn; row++)
{
int portalIndex = group * portalsPerColumn * 2 + portalsPerColumn + row;
if (portalIndex >= totalPortals) break;
var world = worldDataList.worlds[portalIndex];
Vector3 position = new Vector3(
groupBaseX + wallThickness / 2f + 0.1f, // 壁の右側
row * spacing.y,
row * spacing.z // Z座標を3ずつずらす
);
GameObject portal = (GameObject)PrefabUtility.InstantiatePrefab(portalPrefab);
if (portal != null)
{
portal.transform.SetParent(parentObject.transform);
portal.transform.localPosition = position;
portal.transform.localRotation = Quaternion.Euler(0, 90, 0); // 左向き(壁に向かって)
VRCPortalMarker marker = portal.GetComponent<VRCPortalMarker>();
if (marker != null)
{
marker.roomId = world.id;
}
portal.name = $"Portal {portalIndex + 1} - {world.name}";
Undo.RegisterCreatedObjectUndo(portal, "Create Portal");
portalsCreated++;
}
}
// 壁を作成(グループの中央)
if (createWalls)
{
// 壁の高さを計算(元の高さの半分)
float wallHeight = (portalsPerColumn * spacing.y + 1f) * 3f; // 元の高さの半分(2倍を削除)
Vector3 wallPosition = new Vector3(
groupBaseX,
wallHeight / 2f, // 壁の中心が壁の高さの半分の位置(底面がY=0)
(portalsPerColumn - 1) * spacing.z / 2f // Z座標も中央に
);
GameObject wall = GameObject.CreatePrimitive(PrimitiveType.Cube);
wall.transform.SetParent(parentObject.transform);
wall.transform.localPosition = wallPosition;
wall.transform.localScale = new Vector3(
wallThickness,
wallHeight, // 調整された高さを使用
portalsPerColumn * spacing.z + 3f // Z方向のサイズも調整
);
wall.name = $"Wall {group + 1}";
Renderer renderer = wall.GetComponent<Renderer>();
if (renderer != null)
{
renderer.material.color = wallColor;
}
Undo.RegisterCreatedObjectUndo(wall, "Create Wall");
wallsCreated++;
}
}
EditorUtility.DisplayDialog("Success",
$"Created {portalsCreated} portals and {wallsCreated} walls under '{parentObjectName}' successfully!",
"OK");
}
private Vector3 CalculateStandardPosition(int index)
{
if (useGridLayout)
{
int col = index / portalsPerColumn;
int row = index % portalsPerColumn;
return startPosition + new Vector3(
col * spacing.x,
row * spacing.y,
col * spacing.z
);
}
else
{
return startPosition + (spacing * index);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment