Last active
August 2, 2025 05:33
-
-
Save gitcrtn/5e2e7f4d64bfa6ca26b657fb352de66b to your computer and use it in GitHub Desktop.
VRC Portal Auto Placer for Unity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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); // ネットワーク監視終了後に結果表示 | |
| })(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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