Compare commits

...

2 Commits

Author SHA1 Message Date
958ae1cb75 건축 및 고용에 비용 소모 기능 추가 2026-02-02 20:26:44 +09:00
9dea9daaa9 개발자용 멀티플레이 기능 추가 2026-02-02 20:26:28 +09:00
29 changed files with 1913 additions and 24 deletions

View File

@@ -49,10 +49,12 @@
<Analyzer Include="C:\Program Files\Unity\Hub\Editor\6000.3.5f2\Editor\Data\Tools\BuildPipeline\Unity.SourceGenerators\Unity.UIToolkit.SourceGenerator.dll" />
</ItemGroup>
<ItemGroup>
<Compile Include="Assets\Scripts\Editor\NetworkUIBuilder.cs" />
<Compile Include="Assets\Scripts\Editor\MonsterPrefabSetup.cs" />
<Compile Include="Assets\FlatKit\[Render Pipeline] URP\Water\Editor\Tooltips.cs" />
<Compile Include="Assets\Scripts\Editor\EnemyPortalEditor.cs" />
<Compile Include="Assets\Scripts\Editor\ObstacleSpawnerEditor.cs" />
<Compile Include="Assets\Scripts\Editor\QuickNetworkSetupEditor.cs" />
<Compile Include="Assets\Scripts\Editor\IPrefabSetup.cs" />
<Compile Include="Assets\FlatKit\[Render Pipeline] URP\Water\Editor\WaterEditor.cs" />
<Compile Include="Assets\Scripts\Editor\TowerPopulator.cs" />
@@ -65,7 +67,9 @@
<Compile Include="Assets\Scripts\Editor\CreepPrefabSetup.cs" />
<Compile Include="Assets\Scripts\Editor\TemplateCreator.cs" />
<Compile Include="Assets\FlatKit\Shaders\Editor\Tooltips.cs" />
<Compile Include="Assets\Scripts\Editor\NetworkConnectionWindow.cs" />
<Compile Include="Assets\FlatKit\Shaders\Editor\StylizedSurfaceEditor.cs" />
<Compile Include="Assets\Scripts\Editor\NetworkConnectionHelperEditor.cs" />
<Compile Include="Assets\Scripts\Editor\FogOfWarVisibilitySetup.cs" />
<Compile Include="Assets\FlatKit\Shaders\Editor\MaterialPropertyExtensions.cs" />
<Compile Include="Assets\FlatKit\Shaders\Editor\TerrainEditor.cs" />

View File

@@ -86,6 +86,7 @@
<Compile Include="Assets\Scripts\AttackAction.cs" />
<Compile Include="Assets\Scripts\IInteractable.cs" />
<Compile Include="Assets\Scripts\ServerResourceManager.cs" />
<Compile Include="Assets\Scripts\QuickNetworkSetup.cs" />
<Compile Include="Assets\FlatKit\Demos\Common\Scripts\UvScroller.cs" />
<Compile Include="Assets\Data\Scripts\DataClasses\TowerDataExtensions.cs" />
<Compile Include="Assets\Scripts\GlobalTimer.cs" />
@@ -94,6 +95,7 @@
<Compile Include="Assets\Scripts\BuildingHealthBar.cs" />
<Compile Include="Assets\Data\Scripts\DataClasses\CreepData.cs" />
<Compile Include="Assets\Scripts\PlayerSpawnPoint.cs" />
<Compile Include="Assets\Scripts\NetworkJoinUI.cs" />
<Compile Include="Assets\FlatKit\Demos\Common\Scripts\AutoLoadPipelineAsset.cs" />
<Compile Include="Assets\Scripts\AutoHost.cs" />
<Compile Include="Assets\Scripts\MonsterAnimationController.cs" />
@@ -120,6 +122,7 @@
<Compile Include="Assets\Scripts\EnemyAIState.cs" />
<Compile Include="Assets\Scripts\ShortcutNetworkStarter.cs" />
<Compile Include="Assets\Scripts\EquipmentData.cs" />
<Compile Include="Assets\Scripts\NetworkConnectionHelper.cs" />
<Compile Include="Assets\Scripts\NetworkConnectionHandler.cs" />
</ItemGroup>
<ItemGroup>

View File

@@ -3,24 +3,38 @@ using Unity.Netcode;
public class AutoHost : MonoBehaviour
{
// 에디터에서만 작동하도록 설정
void Start()
[Header("Auto Host Settings")]
[SerializeField] private bool enableAutoHost = true;
[SerializeField] private bool onlyInEditor = true;
private void Start()
{
#if UNITY_EDITOR
// 1. NetworkManager가 씬에 존재하는지 확인
if (NetworkManager.Singleton != null)
if (ShouldAutoStart())
{
// 2. 이미 서버나 클라이언트가 실행 중이 아닐 때만 실행
if (!NetworkManager.Singleton.IsServer && !NetworkManager.Singleton.IsClient)
if (NetworkManager.Singleton != null)
{
NetworkManager.Singleton.StartHost();
Debug.Log("<color=yellow><b>[AutoHost]</b> 에디터 전용 호스트 자동 시작됨</color>");
if (!NetworkManager.Singleton.IsServer && !NetworkManager.Singleton.IsClient)
{
NetworkManager.Singleton.StartHost();
Debug.Log("<color=yellow><b>[AutoHost]</b> Auto-host started</color>");
}
}
else
{
Debug.LogError("[AutoHost] NetworkManager not found!");
}
}
else
{
Debug.LogError("[AutoHost] NetworkManager를 찾을 수 없습니다!");
}
}
private bool ShouldAutoStart()
{
if (!enableAutoHost)
return false;
#if UNITY_EDITOR
return true;
#else
return !onlyInEditor;
#endif
}
}

View File

@@ -193,6 +193,22 @@ namespace Northbound
return;
}
// 자원 확인 및 소비
var coreResourceManager = CoreResourceManager.Instance;
if (coreResourceManager == null)
{
Debug.LogWarning("<color=red>[BuildingManager] CoreResourceManager 인스턴스를 찾을 수 없습니다.</color>");
return;
}
if (!coreResourceManager.CanAfford(data.mana))
{
Debug.LogWarning($"<color=yellow>[BuildingManager] 코어 자원이 부족합니다. 필요: {data.mana} (클라이언트: {requestingClientId})</color>");
return;
}
coreResourceManager.SpendResources(data.mana);
// 배치 가능 여부 확인
if (!IsValidPlacement(data, position, rotation, out Vector3 snappedPosition))
{
@@ -345,6 +361,22 @@ namespace Northbound
return;
}
// 자원 확인 및 소비
var coreResourceManager = CoreResourceManager.Instance;
if (coreResourceManager == null)
{
Debug.LogWarning("<color=red>[BuildingManager] CoreResourceManager 인스턴스를 찾을 수 없습니다.</color>");
return;
}
if (!coreResourceManager.CanAfford(data.mana))
{
Debug.LogWarning($"<color=yellow>[BuildingManager] 코어 자원이 부족합니다. 필요: {data.mana}</color>");
return;
}
coreResourceManager.SpendResources(data.mana);
// 토대 프리팹 확인
if (foundationPrefab == null)
{

View File

@@ -328,16 +328,20 @@ namespace Northbound
if (Physics.Raycast(ray, out RaycastHit hit, maxPlacementDistance, groundLayer))
{
// Check if placement is valid
bool isValid = BuildingManager.Instance.IsValidPlacement(data, hit.point, currentRotation, out Vector3 snappedPosition);
// Check affordability
var coreResourceManager = CoreResourceManager.Instance;
bool canAfford = coreResourceManager != null && coreResourceManager.CanAfford(data.mana);
// Update preview position (placementOffset 적용)
previewObject.transform.position = snappedPosition + data.placementOffset;
previewObject.transform.rotation = Quaternion.Euler(0, currentRotation * 90f, 0);
// Update material based on validity
Material targetMat = isValid ? validMaterial : invalidMaterial;
// Update material based on validity and affordability
Material targetMat = (isValid && canAfford) ? validMaterial : invalidMaterial;
foreach (var renderer in previewRenderers)
{
Material[] mats = new Material[renderer.materials.Length];
@@ -462,6 +466,10 @@ namespace Northbound
Vector3 dragEndPosition = hit.point;
List<Vector3> positions = CalculateDragBuildingPositions(dragStartPosition, dragEndPosition, data);
// Check affordability for all buildings
var coreResourceManager = CoreResourceManager.Instance;
bool canAffordAll = coreResourceManager != null && coreResourceManager.CanAfford(data.mana * positions.Count);
// 기존 프리뷰 정리
ClearDragPreviews();
@@ -476,9 +484,9 @@ namespace Northbound
}
bool isValid = BuildingManager.Instance.IsValidPlacement(data, pos, currentRotation, out Vector3 snappedPosition);
GameObject preview = Instantiate(data.prefab);
// Remove NetworkObject and Building components
if (preview.GetComponent<NetworkObject>() != null)
Destroy(preview.GetComponent<NetworkObject>());
@@ -490,7 +498,7 @@ namespace Northbound
preview.transform.rotation = Quaternion.Euler(0, currentRotation * 90f, 0);
// Apply materials
Material targetMat = isValid ? validMaterial : invalidMaterial;
Material targetMat = (isValid && canAffordAll) ? validMaterial : invalidMaterial;
Renderer[] renderers = preview.GetComponentsInChildren<Renderer>();
foreach (var renderer in renderers)
{
@@ -569,6 +577,14 @@ namespace Northbound
TowerData selectedData = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
// Check affordability
var coreResourceManager = CoreResourceManager.Instance;
if (coreResourceManager == null || !coreResourceManager.CanAfford(selectedData.mana * dragBuildingPositions.Count))
{
Debug.Log("<color=yellow>[BuildingPlacement] 자원이 부족합니다.</color>");
return;
}
int successCount = 0;
foreach (var position in dragBuildingPositions)
{
@@ -608,7 +624,15 @@ namespace Northbound
if (Physics.Raycast(ray, out RaycastHit hit, maxPlacementDistance, groundLayer))
{
TowerData selectedData = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
// Check affordability
var coreResourceManager = CoreResourceManager.Instance;
if (coreResourceManager != null && !coreResourceManager.CanAfford(selectedData.mana))
{
Debug.Log("<color=yellow>[BuildingPlacement] 자원이 부족합니다.</color>");
return;
}
if (BuildingManager.Instance.IsValidPlacement(selectedData, hit.point, currentRotation, out Vector3 groundPosition))
{
// 토대 배치 요청

View File

@@ -68,6 +68,28 @@ namespace Northbound
InitializeSlots();
}
private void Update()
{
if (quickslotPanel != null && quickslotPanel.activeSelf)
{
UpdateCostDisplays();
}
}
/// <summary>
/// 모든 슬롯의 비용 표시 업데이트
/// </summary>
public void UpdateCostDisplays()
{
foreach (var slot in slotButtons)
{
if (slot != null)
{
slot.UpdateCostDisplay();
}
}
}
/// <summary>
/// 퀵슬롯 Input Actions 초기화 및 구독 (직접 참조)
/// </summary>

View File

@@ -52,6 +52,20 @@ namespace Northbound
UpdateVisuals();
}
/// <summary>
/// 비용 표시 업데이트 (자원 변화 시 호출)
/// </summary>
public void UpdateCostDisplay()
{
if (nameText != null && buildingData != null)
{
var coreResourceManager = CoreResourceManager.Instance;
bool canAfford = coreResourceManager != null && coreResourceManager.CanAfford(buildingData.mana);
string costText = canAfford ? $"{buildingData.mana}" : $"<color=red>{buildingData.mana}</color>";
nameText.text = $"{buildingData.buildingName}\n비용: {costText}";
}
}
/// <summary>
/// UI 업데이트
/// </summary>
@@ -75,7 +89,10 @@ namespace Northbound
// 이름 설정
if (nameText != null)
{
nameText.text = buildingData.buildingName;
var coreResourceManager = CoreResourceManager.Instance;
bool canAfford = coreResourceManager != null && coreResourceManager.CanAfford(buildingData.mana);
string costText = canAfford ? $"{buildingData.mana}" : $"<color=red>{buildingData.mana}</color>";
nameText.text = $"{buildingData.buildingName}\n비용: {costText}";
}
// 배경 색상

View File

@@ -0,0 +1,176 @@
using UnityEngine;
using UnityEditor;
using Unity.Netcode;
namespace Northbound.Editor
{
[CustomEditor(typeof(NetworkConnectionHelper))]
public class NetworkConnectionHelperEditor : UnityEditor.Editor
{
private NetworkConnectionHelper _helper;
private GUIStyle _statusStyle;
private void OnEnable()
{
_helper = (NetworkConnectionHelper)target;
}
public override void OnInspectorGUI()
{
_statusStyle = new GUIStyle(EditorStyles.boldLabel);
_statusStyle.alignment = TextAnchor.MiddleCenter;
_statusStyle.fontSize = 12;
DrawDefaultInspector();
EditorGUILayout.Space(10);
DrawConnectionControls();
EditorGUILayout.Space(10);
DrawStatus();
EditorGUILayout.Space(10);
DrawQuickActions();
}
private void DrawConnectionControls()
{
EditorGUILayout.LabelField("Network Controls", EditorStyles.boldLabel);
EditorGUI.BeginDisabledGroup(IsConnected());
if (GUILayout.Button("Start Host", GUILayout.Height(30)))
{
_helper.StartHost();
}
if (GUILayout.Button("Start Server", GUILayout.Height(30)))
{
_helper.StartServer();
}
if (GUILayout.Button("Start Client", GUILayout.Height(30)))
{
_helper.StartClient();
}
EditorGUI.EndDisabledGroup();
EditorGUI.BeginDisabledGroup(!IsConnected());
if (GUILayout.Button("Disconnect", GUILayout.Height(30)))
{
_helper.Disconnect();
}
EditorGUI.EndDisabledGroup();
}
private void DrawStatus()
{
EditorGUILayout.LabelField("Status", EditorStyles.boldLabel);
string status = GetDetailedStatus();
Color bgColor = GetStatusColor();
var oldBgColor = GUI.backgroundColor;
GUI.backgroundColor = bgColor;
EditorGUILayout.LabelField(status, _statusStyle, GUILayout.Height(25));
GUI.backgroundColor = oldBgColor;
if (NetworkManager.Singleton != null && IsConnected())
{
DrawNetworkInfo();
}
}
private void DrawNetworkInfo()
{
EditorGUILayout.LabelField("Network Information", EditorStyles.boldLabel);
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
if (NetworkManager.Singleton.IsHost)
{
EditorGUILayout.LabelField($"Mode: Host");
}
else if (NetworkManager.Singleton.IsServer)
{
EditorGUILayout.LabelField($"Mode: Server");
}
else if (NetworkManager.Singleton.IsClient)
{
EditorGUILayout.LabelField($"Mode: Client");
}
EditorGUILayout.LabelField($"Connected Clients: {NetworkManager.Singleton.ConnectedClients.Count}");
EditorGUILayout.LabelField($"Local Client ID: {NetworkManager.Singleton.LocalClientId}");
var transport = NetworkManager.Singleton.GetComponent<Unity.Netcode.Transports.UTP.UnityTransport>();
if (transport != null)
{
EditorGUILayout.LabelField($"Address: {transport.ConnectionData.Address}");
EditorGUILayout.LabelField($"Port: {transport.ConnectionData.Port}");
}
}
}
private void DrawQuickActions()
{
EditorGUILayout.LabelField("Quick Actions", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Open Connection Window"))
{
NetworkConnectionWindow.ShowWindow();
}
if (GUILayout.Button("Test Connection"))
{
TestConnection();
}
EditorGUILayout.EndHorizontal();
}
private bool IsConnected()
{
return NetworkManager.Singleton != null &&
(NetworkManager.Singleton.IsClient || NetworkManager.Singleton.IsServer);
}
private string GetDetailedStatus()
{
if (NetworkManager.Singleton == null)
return "NetworkManager Not Found";
if (NetworkManager.Singleton.IsHost)
return "HOSTING";
if (NetworkManager.Singleton.IsServer)
return "SERVER RUNNING";
if (NetworkManager.Singleton.IsClient)
return "CONNECTED";
return "NOT CONNECTED";
}
private Color GetStatusColor()
{
if (NetworkManager.Singleton == null)
return new Color(0.8f, 0.3f, 0.3f);
if (NetworkManager.Singleton.IsHost || NetworkManager.Singleton.IsClient || NetworkManager.Singleton.IsServer)
return new Color(0.3f, 0.8f, 0.3f);
return new Color(0.8f, 0.6f, 0.3f);
}
private void TestConnection()
{
Debug.Log($"[NetworkConnectionHelper] Connection test - Status: {_helper.GetStatus()}");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 36e75e477fa8d69459450bd36b5c6878

View File

@@ -0,0 +1,295 @@
using UnityEngine;
using UnityEditor;
using Unity.Netcode;
using System.IO;
using UnityEngine.SceneManagement;
namespace Northbound.Editor
{
public class NetworkConnectionWindow : UnityEditor.EditorWindow
{
private string _serverIP = "127.0.0.1";
private string _port = "7777";
private bool _isHost = true;
private NetworkConnectionMode _connectionMode = NetworkConnectionMode.Host;
private string _savedSettingsPath;
private enum NetworkConnectionMode
{
Host,
Client,
Server
}
[MenuItem("Window/Network/Connection Manager %#n")]
public static void ShowWindow()
{
var window = GetWindow<NetworkConnectionWindow>("Network Connection");
window.minSize = new Vector2(350, 250);
}
private void OnEnable()
{
_savedSettingsPath = Path.Combine(Application.persistentDataPath, "NetworkConnectionSettings.json");
LoadSettings();
}
private void OnGUI()
{
EditorGUILayout.Space(10);
GUILayout.Label("Network Connection Manager", EditorStyles.boldLabel);
EditorGUILayout.Space(5);
DrawConnectionModeUI();
EditorGUILayout.Space(10);
DrawConnectionSettings();
EditorGUILayout.Space(10);
DrawStatusDisplay();
EditorGUILayout.Space(10);
DrawActionButtons();
EditorGUILayout.Space(10);
DrawSettingsButtons();
}
private void DrawConnectionModeUI()
{
EditorGUILayout.LabelField("Connection Mode", EditorStyles.boldLabel);
_connectionMode = (NetworkConnectionMode)EditorGUILayout.EnumPopup("Mode", _connectionMode);
if (_connectionMode == NetworkConnectionMode.Host)
{
_isHost = true;
}
}
private void DrawConnectionSettings()
{
EditorGUILayout.LabelField("Connection Settings", EditorStyles.boldLabel);
if (_connectionMode == NetworkConnectionMode.Client)
{
_serverIP = EditorGUILayout.TextField("Server IP", _serverIP);
_port = EditorGUILayout.TextField("Port", _port);
}
else if (_connectionMode == NetworkConnectionMode.Host || _connectionMode == NetworkConnectionMode.Server)
{
_port = EditorGUILayout.TextField("Port", _port);
}
if (GUI.changed)
{
SaveSettings();
}
}
private void DrawStatusDisplay()
{
EditorGUILayout.LabelField("Status", EditorStyles.boldLabel);
string status = GetNetworkStatus();
Color statusColor = GetStatusColor();
var oldColor = GUI.color;
GUI.color = statusColor;
EditorGUILayout.LabelField(status, EditorStyles.helpBox);
GUI.color = oldColor;
}
private void DrawActionButtons()
{
EditorGUI.BeginDisabledGroup(IsNetworkActive());
if (GUILayout.Button(_connectionMode == NetworkConnectionMode.Client ? "Connect" : "Start", GUILayout.Height(30)))
{
StartConnection();
}
EditorGUI.EndDisabledGroup();
EditorGUI.BeginDisabledGroup(!IsNetworkActive());
if (GUILayout.Button("Disconnect", GUILayout.Height(30)))
{
Disconnect();
}
EditorGUI.EndDisabledGroup();
}
private void DrawSettingsButtons()
{
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Save Settings"))
{
SaveSettings();
Debug.Log("[NetworkConnectionWindow] Settings saved.");
}
if (GUILayout.Button("Load Settings"))
{
LoadSettings();
Debug.Log("[NetworkConnectionWindow] Settings loaded.");
}
EditorGUILayout.EndHorizontal();
}
private void StartConnection()
{
if (NetworkManager.Singleton == null)
{
EditorUtility.DisplayDialog("Error", "NetworkManager not found in the scene!", "OK");
return;
}
ushort port;
if (!ushort.TryParse(_port, out port))
{
EditorUtility.DisplayDialog("Error", "Invalid port number!", "OK");
return;
}
NetworkManager.Singleton.GetComponent<Unity.Netcode.Transports.UTP.UnityTransport>()?.SetConnectionData(
_serverIP,
port
);
switch (_connectionMode)
{
case NetworkConnectionMode.Host:
NetworkManager.Singleton.StartHost();
Debug.Log($"[NetworkConnectionWindow] Started Host on port {port}");
break;
case NetworkConnectionMode.Client:
NetworkManager.Singleton.StartClient();
Debug.Log($"[NetworkConnectionWindow] Started Client connecting to {_serverIP}:{port}");
break;
case NetworkConnectionMode.Server:
NetworkManager.Singleton.StartServer();
Debug.Log($"[NetworkConnectionWindow] Started Server on port {port}");
break;
}
}
private void Disconnect()
{
if (NetworkManager.Singleton != null)
{
NetworkManager.Singleton.Shutdown();
Debug.Log("[NetworkConnectionWindow] Disconnected");
}
}
private bool IsNetworkActive()
{
return NetworkManager.Singleton != null &&
(NetworkManager.Singleton.IsClient || NetworkManager.Singleton.IsServer);
}
private string GetNetworkStatus()
{
if (NetworkManager.Singleton == null)
return "NetworkManager not found";
if (NetworkManager.Singleton.IsHost)
return $"Hosting (Port: {GetActivePort()})";
if (NetworkManager.Singleton.IsServer)
return $"Server (Port: {GetActivePort()})";
if (NetworkManager.Singleton.IsClient)
return $"Client connected to {GetActiveIP()}:{GetActivePort()}";
return "Not connected";
}
private Color GetStatusColor()
{
if (NetworkManager.Singleton == null)
return Color.red;
if (NetworkManager.Singleton.IsHost || NetworkManager.Singleton.IsClient || NetworkManager.Singleton.IsServer)
return new Color(0f, 0.7f, 0f);
return new Color(1f, 0.7f, 0f);
}
private string GetActiveIP()
{
if (NetworkManager.Singleton == null) return "N/A";
var transport = NetworkManager.Singleton.GetComponent<Unity.Netcode.Transports.UTP.UnityTransport>();
if (transport != null)
{
return transport.ConnectionData.Address;
}
return "N/A";
}
private string GetActivePort()
{
if (NetworkManager.Singleton == null) return "N/A";
var transport = NetworkManager.Singleton.GetComponent<Unity.Netcode.Transports.UTP.UnityTransport>();
if (transport != null)
{
return transport.ConnectionData.Port.ToString();
}
return "N/A";
}
private void SaveSettings()
{
try
{
string json = $"{{\"serverIP\":\"{_serverIP}\",\"port\":\"{_port}\",\"connectionMode\":{_connectionMode}}}";
File.WriteAllText(_savedSettingsPath, json);
}
catch (System.Exception e)
{
Debug.LogError($"[NetworkConnectionWindow] Failed to save settings: {e.Message}");
}
}
private void LoadSettings()
{
try
{
if (File.Exists(_savedSettingsPath))
{
string json = File.ReadAllText(_savedSettingsPath);
var settings = JsonUtility.FromJson<ConnectionSettings>(json);
_serverIP = settings.serverIP;
_port = settings.port;
_connectionMode = (NetworkConnectionMode)settings.connectionMode;
}
}
catch (System.Exception e)
{
Debug.LogError($"[NetworkConnectionWindow] Failed to load settings: {e.Message}");
}
}
private void Update()
{
if (IsNetworkActive())
{
Repaint();
}
}
[System.Serializable]
private class ConnectionSettings
{
public string serverIP;
public string port;
public int connectionMode;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b7b2610829b9bba4f801fb9d527963c9

View File

@@ -0,0 +1,176 @@
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;
namespace Northbound.Editor
{
public class NetworkUIBuilder
{
[MenuItem("GameObject/Network/Create Network Join UI")]
public static void CreateNetworkJoinUI()
{
Canvas canvas = FindOrCreateCanvas();
GameObject panel = CreatePanel(canvas.transform);
InputField ipInput = CreateInputField(panel.transform, "IP Input", "127.0.0.1", 150);
InputField portInput = CreateInputField(panel.transform, "Port Input", "7777", 100);
Button joinButton = CreateButton(panel.transform, "Join", new Vector2(0, -80));
Button hostButton = CreateButton(panel.transform, "Host", new Vector2(-60, -80));
Button disconnectButton = CreateButton(panel.transform, "Disconnect", new Vector2(60, -80));
Text statusText = CreateStatusText(panel.transform);
NetworkJoinUI networkJoinUI = panel.AddComponent<NetworkJoinUI>();
networkJoinUI.SetUIReferences(panel, ipInput, portInput, joinButton, hostButton, disconnectButton, statusText);
Selection.activeGameObject = panel;
Debug.Log("[NetworkUIBuilder] Network Join UI created!");
}
private static Canvas FindOrCreateCanvas()
{
Canvas[] canvases = Object.FindObjectsByType<Canvas>(FindObjectsSortMode.None);
if (canvases.Length > 0)
{
return canvases[0];
}
GameObject canvasObj = new GameObject("NetworkCanvas");
Canvas canvas = canvasObj.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvasObj.AddComponent<CanvasScaler>();
canvasObj.AddComponent<GraphicRaycaster>();
return canvas;
}
private static GameObject CreatePanel(Transform parent)
{
GameObject panel = new GameObject("NetworkJoinPanel");
panel.transform.SetParent(parent, false);
RectTransform rect = panel.AddComponent<RectTransform>();
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.sizeDelta = new Vector2(300, 250);
rect.anchoredPosition = Vector2.zero;
Image image = panel.AddComponent<Image>();
image.color = new Color(0.1f, 0.1f, 0.1f, 0.95f);
return panel;
}
private static InputField CreateInputField(Transform parent, string name, string placeholder, float width)
{
GameObject fieldObj = new GameObject(name);
fieldObj.transform.SetParent(parent, false);
RectTransform rect = fieldObj.AddComponent<RectTransform>();
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.sizeDelta = new Vector2(width, 30);
int yPos = name.Contains("IP") ? 80 : 40;
rect.anchoredPosition = new Vector2(0, yPos);
Image bgImage = fieldObj.AddComponent<Image>();
bgImage.color = new Color(0.2f, 0.2f, 0.2f, 1f);
InputField inputField = fieldObj.AddComponent<InputField>();
inputField.textComponent = CreateTextComponent(fieldObj.transform, "");
inputField.text = placeholder;
inputField.contentType = InputField.ContentType.Standard;
Text placeholderText = CreateTextComponent(fieldObj.transform, placeholder);
placeholderText.color = new Color(0.5f, 0.5f, 0.5f, 0.5f);
inputField.placeholder = placeholderText;
Text label = CreateTextComponent(parent, name.ToUpper());
RectTransform labelRect = label.GetComponent<RectTransform>();
labelRect.anchoredPosition = new Vector2(-width / 2 - 30, yPos);
return inputField;
}
private static Button CreateButton(Transform parent, string text, Vector2 position)
{
GameObject buttonObj = new GameObject(text + "Button");
buttonObj.transform.SetParent(parent, false);
RectTransform rect = buttonObj.AddComponent<RectTransform>();
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.sizeDelta = new Vector2(80, 30);
rect.anchoredPosition = position;
Image bgImage = buttonObj.AddComponent<Image>();
bgImage.color = new Color(0.3f, 0.5f, 0.8f, 1f);
Button button = buttonObj.AddComponent<Button>();
Text buttonText = CreateTextComponent(buttonObj.transform, text);
buttonText.alignment = TextAnchor.MiddleCenter;
return button;
}
private static Text CreateTextComponent(Transform parent, string text)
{
GameObject textObj = new GameObject("Text");
textObj.transform.SetParent(parent, false);
RectTransform rect = textObj.AddComponent<RectTransform>();
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = Vector2.zero;
rect.offsetMax = Vector2.zero;
Text textComponent = textObj.AddComponent<Text>();
textComponent.text = text;
textComponent.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
textComponent.fontSize = 14;
textComponent.color = Color.white;
textComponent.alignment = TextAnchor.MiddleCenter;
return textComponent;
}
private static Text CreateStatusText(Transform parent)
{
GameObject textObj = new GameObject("StatusText");
textObj.transform.SetParent(parent, false);
RectTransform rect = textObj.AddComponent<RectTransform>();
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.sizeDelta = new Vector2(280, 30);
rect.anchoredPosition = new Vector2(0, -120);
Text textComponent = textObj.AddComponent<Text>();
textComponent.text = "Not connected";
textComponent.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
textComponent.fontSize = 12;
textComponent.color = Color.yellow;
textComponent.alignment = TextAnchor.MiddleCenter;
return textComponent;
}
[MenuItem("GameObject/Network/Create Network Helper")]
public static void CreateNetworkHelper()
{
GameObject helperObj = new GameObject("NetworkConnectionHelper");
helperObj.AddComponent<NetworkConnectionHelper>();
Selection.activeGameObject = helperObj;
Debug.Log("[NetworkUIBuilder] Network Connection Helper created!");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 010611d682d1a064caa47bc3667f0675

View File

@@ -0,0 +1,93 @@
using UnityEngine;
using UnityEditor;
namespace Northbound.Editor
{
public class QuickNetworkSetupEditor
{
[MenuItem("Tools/Network/Quick Setup for IP Connection")]
public static void QuickSetup()
{
bool disableAutoHost = EditorUtility.DisplayDialog(
"Quick Network Setup",
"This will set up your scene for manual IP-based network connections.\n\n" +
"It will:\n" +
"- Create a NetworkConnectionHelper component\n" +
"- Disable AutoHost (to prevent auto-start)\n\n" +
"Do you want to disable AutoHost?",
"Yes, disable AutoHost",
"No, keep AutoHost"
);
CreateNetworkHelper();
if (disableAutoHost)
{
DisableAutoHost();
}
EditorUtility.DisplayDialog(
"Setup Complete",
"Quick network setup complete!\n\n" +
"Next steps:\n" +
"1. Open Window > Network > Connection Manager\n" +
"2. Or use the NetworkConnectionHelper in Inspector\n" +
"3. Start Host on one instance\n" +
"4. Start Client on other instances with the Host's IP",
"OK"
);
}
[MenuItem("Tools/Network/Create Connection Helper")]
public static void CreateNetworkHelper()
{
if (Object.FindObjectOfType<NetworkConnectionHelper>() == null)
{
GameObject helperObj = new GameObject("NetworkConnectionHelper");
helperObj.AddComponent<NetworkConnectionHelper>();
Selection.activeGameObject = helperObj;
Debug.Log("[QuickNetworkSetup] NetworkConnectionHelper created");
}
else
{
Debug.Log("[QuickNetworkSetup] NetworkConnectionHelper already exists");
}
}
[MenuItem("Tools/Network/Disable AutoHost")]
public static void DisableAutoHost()
{
AutoHost[] autoHosts = Object.FindObjectsByType<AutoHost>(FindObjectsSortMode.None);
if (autoHosts.Length > 0)
{
foreach (var autoHost in autoHosts)
{
autoHost.enabled = false;
}
Debug.Log($"[QuickNetworkSetup] Disabled {autoHosts.Length} AutoHost component(s)");
}
else
{
Debug.Log("[QuickNetworkSetup] No AutoHost components found");
}
}
[MenuItem("Tools/Network/Enable AutoHost")]
public static void EnableAutoHost()
{
AutoHost[] autoHosts = Object.FindObjectsByType<AutoHost>(FindObjectsSortMode.None);
if (autoHosts.Length > 0)
{
foreach (var autoHost in autoHosts)
{
autoHost.enabled = true;
}
Debug.Log($"[QuickNetworkSetup] Enabled {autoHosts.Length} AutoHost component(s)");
}
else
{
Debug.Log("[QuickNetworkSetup] No AutoHost components found");
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d84f5c07a6e69614dad5178e0f994335

View File

@@ -0,0 +1,176 @@
# 네트워크 연결 기능 사용 가이드
에디터와 빌드 모두에서 IP 주소를 통해 서버에 접속할 수 있는 기능이 추가되었습니다.
## 새로 추가된 기능
### 1. 네트워크 연결 윈도우 (NetworkConnectionWindow)
GUI 기반의 전용 에디터 윈도우
**사용 방법:**
- 메뉴: `Window > Network > Connection Manager` 또는 단축키 `Ctrl+Shift+N` / `Cmd+Shift+N`
- 기능:
- 연결 모드 선택 (호스트/클라이언트/서버)
- 서버 IP와 포트 입력
- 연결 시작/중지
- 연결 상태 확인
- 연결 설정 저장/로드
### 2. 네트워크 연결 헬퍼 (NetworkConnectionHelper)
커스텀 인스펙터가 포함된 컴포넌트
**사용 방법:**
1. 씬의 게임오브젝트에 `NetworkConnectionHelper` 컴포넌트 추가
2. 인스펙터에서 설정:
- 서버 IP (클라이언트 모드용)
- 포트 번호
- 자동 시작 옵션
3. 인스펙터 버튼으로 제어:
- Start Host
- Start Server
- Start Client
- Disconnect
4. 실시간 연결 상태와 네트워크 정보 확인
### 3. 네트워크 조인 UI (NetworkJoinUI)
인게임 UI로 IP를 통한 접속 지원 (에디터/빌드 모두 작동)
**사용 방법:**
1. 메뉴에서 `GameObject > Network > Create Network Join UI` 선택
2. `J` 키로 UI 패널 토글
3. UI 버튼으로 접속:
- Join: 입력된 IP로 접속
- Host: 호스트로 시작
- Disconnect: 연결 종료
### 4. AutoHost 업데이트
에디터 모드에서 자동 호스트 시작 (설정 가능)
**사용 방법:**
- `AutoHost` 컴포넌트 확인
- 설정:
- `Enable Auto Host`: 자동 시작 활성화/비활성화
- `Only In Editor`: 에디터에서만 자동 시작 (빌드에서는 비활성화)
## 빠른 설정
### 방법 A: 퀵 설정 메뉴 사용 (추천)
```
Tools > Network > Quick Setup for IP Connection
```
이 옵션은:
- NetworkConnectionHelper 생성
- AutoHost 비활성화 (선택 사항)
### 방법 B: 수동 설정
1. `Tools > Network > Create Connection Helper`로 헬퍼 생성
2. `Tools > Network > Disable AutoHost`로 자동 시작 비활성화
## 멀티플레이어 테스트 방법
### 1. 로컬 테스트 (같은 컴퓨터)
1. 윈도우: `Window > Network > Connection Manager` 열기
2. **인스턴스 1:**
- 모드: Host 선택
- 포트: 7777 (기본값)
- "Start" 클릭
3. **인스턴스 2:**
- 모드: Client 선택
- IP: 127.0.0.1 또는 localhost
- 포트: 7777
- "Connect" 클릭
### 2. 네트워크 테스트 (같은 네트워크)
1. 호스트 컴퓨터에서 로컬 IP 확인 (cmd에서 `ipconfig` 실행)
2. **호스트:**
- Connection Manager에서 Host 모드로 시작
3. **클라이언트:**
- Connection Manager에서 Client 모드 선택
- IP: 호스트 컴퓨터의 로컬 IP (예: 192.168.1.100)
- 포트: 7777
- "Connect" 클릭
### 3. 인스펙터로 테스트
1. `NetworkConnectionHelper` 컴포넌트 추가
2. 인스펙터 버튼으로 호스트/클라이언트 시작
3. 상태와 연결 정보 확인
## 연결 모드 설명
### Host
- 서버와 클라이언트 모두 역할 수행
- 멀티플레이어 게임 호스팅
- 플레이어들이 당신의 IP로 접속
### Client
- 기존 서버에 접속
- 서버 IP 주소 필요
### Server
- 전용 서버 모드 (플레이어 없음)
- 전용 서버용
## 네트워크 정보 표시
모든 연결 방법에서 다음을 표시:
- 현재 모드 (호스트/클라이언트/서버)
- 연결 상태
- IP 주소와 포트
- 접속된 클라이언트 수
- 로컬 클라이언트 ID
## 팁
1. **로컬 테스트:**
- `127.0.0.1` 또는 `localhost` 사용
- 같은 네트워크에서 테스트할 때 로컬 IP 사용
2. **기본 포트:**
- 기본 포트는 `7777`
- 연결 컴포넌트 설정에서 변경 가능
3. **방화벽:**
- 네트워크 연결을 위해 포트 개방 필요
- Windows 방화벽이 연결을 차단할 수 있음
4. **여러 에디터 인스턴스:**
- Unity의 멀티플레이 툴 사용
- 또는 여러 빌드 실행
## 기존 시스템과의 호환성
새 기능들은 기존 시스템과 통합:
- 플레이어 스폰은 `NetworkConnectionHandler`가 자동 처리
- 연결 승인은 `NetworkConnectionHandler`가 담당
- 기존 멀티플레이어 시스템과 호환
- 기존 코드 수정 불필요
## 파일 목록
### 새로 생성된 파일
- `Assets/Scripts/NetworkConnectionHelper.cs` - 네트워크 헬퍼 컴포넌트
- `Assets/Scripts/NetworkJoinUI.cs` - 인게임 연결 UI
- `Assets/Scripts/QuickNetworkSetup.cs` - 빠른 설정 스크립트
- `Assets/Scripts/Editor/NetworkConnectionWindow.cs` - 연결 관리자 윈도우
- `Assets/Scripts/Editor/NetworkConnectionHelperEditor.cs` - 헬퍼 커스텀 인스펙터
- `Assets/Scripts/Editor/NetworkUIBuilder.cs` - UI 생성 도구
- `Assets/Scripts/Editor/QuickNetworkSetupEditor.cs` - 빠른 설정 에디터 도구
### 수정된 파일
- `Assets/Scripts/AutoHost.cs` - 설정 가능한 옵션 추가
## 문제 해결
### 연결 실패 시
1. IP 주소와 포트 번호 확인
2. 방화벽 설정 확인
3. 같은 네트워크에 있는지 확인
4. 호스트가 먼저 실행 중인지 확인
### 자동 시작 비활성화 방법
1. `Tools > Network > Disable AutoHost` 선택
2. 또는 `AutoHost` 컴포넌트의 `Enable Auto Host` 체크 해제
### UI가 보이지 않을 때
1. `J` 키 눌러 토글
2. 또는 인스펙터에서 `NetworkJoinUI` 활성화 확인

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9e44a64498019a7468946f609fb954cd
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,137 @@
# Network Connection Features
This project provides multiple ways to connect to multiplayer servers by IP address, both in the editor and in standalone builds.
## Features
### 1. Editor Window (NetworkConnectionWindow)
A dedicated Editor window for managing network connections with a GUI.
**How to Use:**
- Open via: `Window > Network > Connection Manager` or press `Ctrl+Shift+N` / `Cmd+Shift+N`
- Features:
- Select connection mode (Host/Client/Server)
- Enter server IP and port
- Start/Stop network connections
- View connection status
- Save/load connection settings
### 2. NetworkConnectionHelper Component
A MonoBehaviour component with custom inspector for quick network control.
**How to Use:**
1. Add `NetworkConnectionHelper` component to any GameObject in your scene
2. Configure settings in Inspector:
- Server IP (for client mode)
- Port
- Auto-start options
3. Use inspector buttons to:
- Start Host
- Start Server
- Start Client
- Disconnect
4. View real-time connection status and network info
### 3. NetworkJoinUI Component
In-game UI for connecting to servers by IP (works in both editor and standalone builds).
**How to Use:**
1. Create UI elements:
- InputField for IP address
- InputField for port
- Buttons: Join, Host, Disconnect
- Text for status display
2. Add `NetworkJoinUI` component to a GameObject
3. Assign UI references in Inspector
4. Press `J` key (default) to toggle the UI panel
5. Connect using the UI buttons
### 4. AutoHost Component (Updated)
Auto-starts as host in editor mode (configurable).
**How to Use:**
- Add `AutoHost` component to any GameObject
- Settings:
- `Enable Auto Host`: Enable/disable auto-start
- `Only In Editor`: Only auto-start in editor (not in standalone builds)
## Setup Instructions
### For Editor Testing:
**Option A: Using Editor Window (Recommended)**
1. Open `Window > Network > Connection Manager`
2. Select mode: `Host` or `Client`
3. Enter IP/port if needed
4. Click `Start` or `Connect`
**Option B: Using NetworkConnectionHelper**
1. Add `NetworkConnectionHelper` component to your scene
2. Use Inspector buttons to control connections
3. For auto-start on play, enable `Auto Start As Host`
**Option C: Using AutoHost (Original Behavior)**
1. Keep or add `AutoHost` component
2. It will auto-start as host when playing in editor
### For Standalone Builds:
**Option A: Using NetworkJoinUI (In-Game UI)**
1. Add `NetworkJoinUI` component with UI references
2. Build and run
3. Press `J` to open connection panel
4. Enter IP and connect
**Option B: Using NetworkConnectionHelper**
1. Add `NetworkConnectionHelper` component
2. Set `Auto Start As Host = true` and `Only In Editor = false`
3. Build - it will auto-start as host
## Connection Modes
### Host
- Acts as both server and client
- Can host multiplayer games
- Players connect to your IP
### Client
- Connects to an existing server
- Requires server IP address
### Server
- Server-only mode (no player)
- Used for dedicated servers
## Network Info Display
All connection methods display:
- Current mode (Host/Client/Server)
- Connection status
- IP address and port
- Number of connected clients
- Local Client ID
## Tips
1. **Testing Multiplayer Locally:**
- Use `127.0.0.1` or `localhost` for local testing
- Or use your local IP for testing on same network
2. **Default Port:**
- Default port is `7777`
- Change in any connection component's settings
3. **Firewall:**
- Ensure port is open for network connections
- Windows Firewall may block connections
4. **Multiple Editor Instances:**
- Use Unity's `ParrelSync` or run multiple standalone builds
- Each instance needs different settings
## Integration
All features integrate with existing `NetworkManager` and `NetworkConnectionHandler`:
- Player spawning is handled automatically
- Connection approval is handled by `NetworkConnectionHandler`
- Compatible with existing multiplayer systems

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f75d808bc7fb71441b7c599cf75ab9a6
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,192 @@
# Network Connection Feature Summary
## Overview
Added comprehensive IP-based network connection features for both editor testing and standalone builds.
## Files Created
### Runtime Components
1. **NetworkConnectionHelper.cs** (3.6KB)
- MonoBehaviour component for network connection management
- Start Host/Server/Client methods
- Auto-start configuration
- Status monitoring
2. **NetworkJoinUI.cs** (7.7KB)
- In-game UI component for IP connection
- Works in both editor and standalone builds
- Toggle panel with J key
- Supports Join/Host/Disconnect
3. **QuickNetworkSetup.cs** (1.6KB)
- Quick scene setup script
- Creates NetworkConnectionHelper
- Optionally disables AutoHost
### Editor Tools
4. **NetworkConnectionWindow.cs** (9.2KB)
- Dedicated Editor window for network management
- GUI-based IP/port configuration
- Connection mode selection
- Status display and settings persistence
5. **NetworkConnectionHelperEditor.cs** (5.8KB)
- Custom Inspector for NetworkConnectionHelper
- Quick action buttons
- Real-time status display
- Network information panel
6. **NetworkUIBuilder.cs** (7.4KB)
- Automated UI creation for NetworkJoinUI
- Creates complete UI structure
- Menu integration
7. **QuickNetworkSetupEditor.cs** (2.0KB)
- Menu items for quick setup
- Tools menu integration
- Enable/Disable AutoHost functionality
### Documentation
8. **NETWORK_CONNECTION_README.md**
- Comprehensive English documentation
- Setup instructions
- Usage examples
9. **NETWORK_CONNECTION_GUIDE_KO.md**
- Korean translation of documentation
- Step-by-step guide
- Troubleshooting section
## Files Modified
1. **AutoHost.cs**
- Made auto-start configurable
- Added editor-only option
- Added enable/disable toggle
## Features Added
### 1. Editor Window (NetworkConnectionWindow)
- Open via: Window > Network > Connection Manager (Ctrl+Shift+N)
- Connection modes: Host/Client/Server
- IP and port configuration
- Start/Stop connections
- Real-time status
- Settings persistence
### 2. NetworkConnectionHelper Component
- Add to any GameObject in scene
- Custom Inspector with quick buttons
- Auto-start configuration
- Status display
- Network information panel
### 3. In-Game UI (NetworkJoinUI)
- Works in editor and standalone builds
- IP/port input fields
- Join/Host/Disconnect buttons
- Status display
- Toggle with J key
- Auto-generated via menu
### 4. Quick Setup Tools
- Tools > Network > Quick Setup for IP Connection
- Automatically disables AutoHost
- Creates NetworkConnectionHelper
## Integration
All new features integrate seamlessly with existing systems:
- **NetworkManager**: Works with Unity Netcode for GameObjects
- **NetworkConnectionHandler**: Handles player spawning and approval
- **AutoHost**: Configurable to avoid conflicts
- **Transport**: Uses Unity Transport (UTP)
## Testing Workflow
### Method 1: Editor Window
1. Window > Network > Connection Manager
2. Select Host on instance 1
3. Select Client on instance 2 with Host's IP
4. Start/Connect
### Method 2: NetworkConnectionHelper
1. Add component to scene
2. Use Inspector buttons
3. View status in real-time
### Method 3: Quick Setup
1. Tools > Network > Quick Setup for IP Connection
2. Use preferred connection method
### Method 4: In-Game UI
1. GameObject > Network > Create Network Join UI
2. Press J to open
3. Enter IP and connect
## Configuration
### Default Settings
- Default IP: 127.0.0.1
- Default Port: 7777
- Toggle Key: J (for UI)
### Options
- Auto-start: Enable/disable in AutoHost
- Editor-only: Configure per component
- Persistence: Save/load in Editor Window
## Benefits
1. **Easy Testing**: No standalone build required
2. **Team Collaboration**: Join teammates by IP
3. **Production Ready**: Works in standalone builds
4. **Flexible**: Multiple connection methods
5. **Intuitive**: GUI-based interfaces
6. **Integrated**: Works with existing systems
## Future Enhancements
Potential improvements:
- Server list browsing
- Favorites/Saved connections
- LAN discovery
- Connection history
- Advanced transport settings
- Password protection
## Usage Example
### Editor Testing
```csharp
// Instance 1 (Host)
Tools > Network > Quick Setup for IP Connection
Window > Network > Connection Manager > Start Host
// Instance 2 (Client)
Tools > Network > Quick Setup for IP Connection
Window > Network > Connection Manager > Connect to 127.0.0.1:7777
```
### Standalone Build
```csharp
// Build with NetworkJoinUI component
Press J to open connection panel
Enter IP: 192.168.1.X
Click Join
```
## Compatibility
- Unity Netcode for GameObjects 2.8.1
- Unity Transport (UTP)
- Unity 2022.x+
- Windows/Mac/Linux
## Notes
- All components are optional
- AutoHost can be disabled
- Existing AutoHost behavior preserved (when enabled)
- No breaking changes to existing code
- Works with custom scenes and setups

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ef125ff435342b64f86eeea8dbef234f
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,117 @@
using UnityEngine;
using Unity.Netcode;
namespace Northbound
{
public class NetworkConnectionHelper : MonoBehaviour
{
[Header("Connection Settings")]
[SerializeField] private string serverIP = "127.0.0.1";
[SerializeField] private ushort port = 7777;
[Header("Auto Start")]
[SerializeField] private bool autoStartAsHost = false;
[SerializeField] private bool onlyInEditor = true;
private void Start()
{
if (autoStartAsHost && ShouldAutoStart())
{
StartHost();
}
}
private bool ShouldAutoStart()
{
#if UNITY_EDITOR
return true;
#else
return !onlyInEditor;
#endif
}
public void StartHost()
{
if (NetworkManager.Singleton == null)
{
Debug.LogError("[NetworkConnectionHelper] NetworkManager not found!");
return;
}
ConfigureTransport("0.0.0.0", port);
if (!NetworkManager.Singleton.IsServer && !NetworkManager.Singleton.IsClient)
{
NetworkManager.Singleton.StartHost();
Debug.Log($"[NetworkConnectionHelper] Started Host on port {port}");
}
}
public void StartClient()
{
if (NetworkManager.Singleton == null)
{
Debug.LogError("[NetworkConnectionHelper] NetworkManager not found!");
return;
}
ConfigureTransport(serverIP, port);
if (!NetworkManager.Singleton.IsClient && !NetworkManager.Singleton.IsServer)
{
NetworkManager.Singleton.StartClient();
Debug.Log($"[NetworkConnectionHelper] Connecting to {serverIP}:{port}");
}
}
public void StartServer()
{
if (NetworkManager.Singleton == null)
{
Debug.LogError("[NetworkConnectionHelper] NetworkManager not found!");
return;
}
ConfigureTransport("0.0.0.0", port);
if (!NetworkManager.Singleton.IsServer && !NetworkManager.Singleton.IsClient)
{
NetworkManager.Singleton.StartServer();
Debug.Log($"[NetworkConnectionHelper] Started Server on port {port}");
}
}
public void Disconnect()
{
if (NetworkManager.Singleton != null)
{
NetworkManager.Singleton.Shutdown();
Debug.Log("[NetworkConnectionHelper] Disconnected");
}
}
public string GetStatus()
{
if (NetworkManager.Singleton == null)
return "NetworkManager not found";
if (NetworkManager.Singleton.IsHost)
return "Hosting";
if (NetworkManager.Singleton.IsServer)
return "Server";
if (NetworkManager.Singleton.IsClient)
return "Client";
return "Not connected";
}
private void ConfigureTransport(string ip, ushort portNum)
{
var transport = NetworkManager.Singleton.GetComponent<Unity.Netcode.Transports.UTP.UnityTransport>();
if (transport != null)
{
transport.SetConnectionData(ip, portNum);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 947211167dacac64092d1c6ea2b00af6

View File

@@ -0,0 +1,269 @@
using UnityEngine;
using UnityEngine.UI;
using Unity.Netcode;
namespace Northbound
{
public class NetworkJoinUI : MonoBehaviour
{
[Header("UI References")]
[SerializeField] private GameObject joinPanel;
[SerializeField] private InputField ipInputField;
[SerializeField] private InputField portInputField;
[SerializeField] private Button joinButton;
[SerializeField] private Button hostButton;
[SerializeField] private Button disconnectButton;
[SerializeField] private Text statusText;
[SerializeField] private KeyCode toggleKey = KeyCode.J;
[Header("Settings")]
[SerializeField] private string defaultIP = "127.0.0.1";
[SerializeField] private string defaultPort = "7777";
private bool _isVisible = false;
private void Awake()
{
InitializeUI();
}
private void Start()
{
SetupDefaultValues();
SubscribeToEvents();
UpdateStatus();
}
private void Update()
{
if (Input.GetKeyDown(toggleKey))
{
TogglePanel();
}
if (NetworkManager.Singleton != null &&
(NetworkManager.Singleton.IsClient || NetworkManager.Singleton.IsServer))
{
UpdateStatus();
}
}
private void InitializeUI()
{
if (joinPanel != null)
{
joinPanel.SetActive(false);
}
}
private void SetupDefaultValues()
{
if (ipInputField != null)
{
ipInputField.text = defaultIP;
}
if (portInputField != null)
{
portInputField.text = defaultPort;
}
}
private void SubscribeToEvents()
{
if (joinButton != null)
{
joinButton.onClick.AddListener(OnJoinClicked);
}
if (hostButton != null)
{
hostButton.onClick.AddListener(OnHostClicked);
}
if (disconnectButton != null)
{
disconnectButton.onClick.AddListener(OnDisconnectClicked);
}
}
private void UnsubscribeFromEvents()
{
if (joinButton != null)
{
joinButton.onClick.RemoveListener(OnJoinClicked);
}
if (hostButton != null)
{
hostButton.onClick.RemoveListener(OnHostClicked);
}
if (disconnectButton != null)
{
disconnectButton.onClick.RemoveListener(OnDisconnectClicked);
}
}
private void OnJoinClicked()
{
if (NetworkManager.Singleton == null)
{
ShowError("NetworkManager not found!");
return;
}
string ip = ipInputField != null ? ipInputField.text : defaultIP;
string port = portInputField != null ? portInputField.text : defaultPort;
ushort portNum;
if (!ushort.TryParse(port, out portNum))
{
ShowError("Invalid port number!");
return;
}
if (NetworkManager.Singleton.IsClient || NetworkManager.Singleton.IsServer)
{
ShowError("Already connected!");
return;
}
var transport = NetworkManager.Singleton.GetComponent<Unity.Netcode.Transports.UTP.UnityTransport>();
if (transport != null)
{
transport.SetConnectionData(ip, portNum);
}
NetworkManager.Singleton.StartClient();
UpdateStatus();
Debug.Log($"[NetworkJoinUI] Connecting to {ip}:{port}");
}
private void OnHostClicked()
{
if (NetworkManager.Singleton == null)
{
ShowError("NetworkManager not found!");
return;
}
string port = portInputField != null ? portInputField.text : defaultPort;
ushort portNum;
if (!ushort.TryParse(port, out portNum))
{
ShowError("Invalid port number!");
return;
}
if (NetworkManager.Singleton.IsClient || NetworkManager.Singleton.IsServer)
{
ShowError("Already connected!");
return;
}
var transport = NetworkManager.Singleton.GetComponent<Unity.Netcode.Transports.UTP.UnityTransport>();
if (transport != null)
{
transport.SetConnectionData("0.0.0.0", portNum);
}
NetworkManager.Singleton.StartHost();
UpdateStatus();
Debug.Log($"[NetworkJoinUI] Started Host on port {port}");
}
private void OnDisconnectClicked()
{
if (NetworkManager.Singleton != null)
{
NetworkManager.Singleton.Shutdown();
UpdateStatus();
Debug.Log("[NetworkJoinUI] Disconnected");
}
}
private void UpdateStatus()
{
if (statusText == null) return;
string status = GetNetworkStatus();
statusText.text = status;
}
private string GetNetworkStatus()
{
if (NetworkManager.Singleton == null)
return "NetworkManager not found";
if (NetworkManager.Singleton.IsHost)
return $"Hosting (Port: {GetActivePort()})";
if (NetworkManager.Singleton.IsServer)
return $"Server (Port: {GetActivePort()})";
if (NetworkManager.Singleton.IsClient)
return $"Client connected to {GetActiveIP()}:{GetActivePort()}";
return "Not connected";
}
private string GetActiveIP()
{
if (NetworkManager.Singleton == null) return "N/A";
var transport = NetworkManager.Singleton.GetComponent<Unity.Netcode.Transports.UTP.UnityTransport>();
if (transport != null)
{
return transport.ConnectionData.Address;
}
return "N/A";
}
private string GetActivePort()
{
if (NetworkManager.Singleton == null) return "N/A";
var transport = NetworkManager.Singleton.GetComponent<Unity.Netcode.Transports.UTP.UnityTransport>();
if (transport != null)
{
return transport.ConnectionData.Port.ToString();
}
return "N/A";
}
private void TogglePanel()
{
if (joinPanel != null)
{
_isVisible = !_isVisible;
joinPanel.SetActive(_isVisible);
UpdateStatus();
}
}
private void ShowError(string message)
{
Debug.LogError($"[NetworkJoinUI] {message}");
if (statusText != null)
{
statusText.text = $"Error: {message}";
}
}
private void OnDestroy()
{
UnsubscribeFromEvents();
}
public void SetUIReferences(GameObject panel, InputField ipField, InputField portField, Button joinBtn, Button hostBtn, Button disconnectBtn, Text status)
{
joinPanel = panel;
ipInputField = ipField;
portInputField = portField;
joinButton = joinBtn;
hostButton = hostBtn;
disconnectButton = disconnectBtn;
statusText = status;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 824b1e5b92d2ce946b9f2c121a535c73

View File

@@ -0,0 +1,54 @@
using UnityEngine;
namespace Northbound
{
public class QuickNetworkSetup : MonoBehaviour
{
[Header("Quick Setup Options")]
[Tooltip("Create a NetworkConnectionHelper component")]
[SerializeField] private bool createConnectionHelper = true;
[Tooltip("Disable AutoHost to prevent auto-start")]
[SerializeField] private bool disableAutoHost = true;
private void Awake()
{
SetupScene();
}
private void SetupScene()
{
if (createConnectionHelper)
{
CreateConnectionHelper();
}
if (disableAutoHost)
{
DisableAutoHost();
}
Destroy(this);
}
private void CreateConnectionHelper()
{
if (FindObjectOfType<NetworkConnectionHelper>() == null)
{
GameObject helperObj = new GameObject("NetworkConnectionHelper");
helperObj.AddComponent<NetworkConnectionHelper>();
Debug.Log("[QuickNetworkSetup] NetworkConnectionHelper created");
}
}
private void DisableAutoHost()
{
AutoHost[] autoHosts = FindObjectsByType<AutoHost>(FindObjectsSortMode.None);
foreach (var autoHost in autoHosts)
{
autoHost.enabled = false;
Debug.Log("[QuickNetworkSetup] AutoHost disabled");
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 572b945beced27f418a84bba613ff5b9

View File

@@ -20,6 +20,7 @@ namespace Northbound
public float followDistance = 3f;
public float movementSpeed = 3.5f;
public int resourcesPerMining = 5;
public int recruitmentCost = 10;
[Header("Interaction")]
public string interactionAnimationTrigger = "Recruit";
@@ -320,7 +321,17 @@ namespace Northbound
public bool CanInteract(ulong playerId)
{
return _ownerPlayerId.Value == ulong.MaxValue || _ownerPlayerId.Value == playerId;
if (_ownerPlayerId.Value != ulong.MaxValue && _ownerPlayerId.Value != playerId)
return false;
if (_ownerPlayerId.Value == ulong.MaxValue)
{
var coreResourceManager = CoreResourceManager.Instance;
if (coreResourceManager != null && !coreResourceManager.CanAfford(recruitmentCost))
return false;
}
return true;
}
public void Interact(ulong playerId)
@@ -348,6 +359,20 @@ namespace Northbound
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RecruitWorkerServerRpc(ulong playerId, ulong workerNetObjectId)
{
var coreResourceManager = CoreResourceManager.Instance;
if (coreResourceManager == null)
{
Debug.LogWarning("[Worker] CoreResourceManager 인스턴스를 찾을 수 없습니다.");
return;
}
if (!coreResourceManager.CanAfford(recruitmentCost))
{
Debug.LogWarning($"[Worker] 코어 자원이 부족합니다. 필요: {recruitmentCost}");
return;
}
coreResourceManager.SpendResources(recruitmentCost);
_ownerPlayerId.Value = playerId;
SetState(WorkerState.Following);
UpdatePlayerTransform();
@@ -389,7 +414,11 @@ namespace Northbound
{
if (_ownerPlayerId.Value == ulong.MaxValue)
{
return "[E] 워커 채용";
var coreResourceManager = CoreResourceManager.Instance;
if (coreResourceManager != null && !coreResourceManager.CanAfford(recruitmentCost))
return $"자원 부족 (필요: {recruitmentCost})";
return $"[E] 워커 채용 - 비용: {recruitmentCost}";
}
else if (NetworkManager.Singleton != null && _ownerPlayerId.Value == NetworkManager.Singleton.LocalClientId)
{

View File

@@ -11,6 +11,7 @@ namespace Northbound
public float spawnRadius = 2f;
public int maxWorkers = 5;
public float spawnCooldown = 5f;
public int recruitmentCost = 20;
[Header("Interaction")]
public string interactionAnimationTrigger = "Build";
@@ -59,6 +60,10 @@ namespace Northbound
if (workerPrefab == null)
return false;
var coreResourceManager = CoreResourceManager.Instance;
if (coreResourceManager != null && !coreResourceManager.CanAfford(recruitmentCost))
return false;
return true;
}
@@ -82,6 +87,21 @@ namespace Northbound
return;
}
var coreResourceManager = CoreResourceManager.Instance;
if (coreResourceManager == null)
{
Debug.LogWarning("[WorkerSpawner] CoreResourceManager 인스턴스를 찾을 수 없습니다.");
return;
}
if (!coreResourceManager.CanAfford(recruitmentCost))
{
Debug.LogWarning($"[WorkerSpawner] 코어 자원이 부족합니다. 필요: {recruitmentCost}");
return;
}
coreResourceManager.SpendResources(recruitmentCost);
Vector3 spawnPosition = spawnPoint != null ? spawnPoint.position : transform.position;
float randomAngle = Random.Range(0f, 360f);
@@ -185,7 +205,11 @@ namespace Northbound
if (cooldownRemaining > 0)
return $"워커 생성 대기 중 ({cooldownRemaining:F1}s)";
return $"{interactionPrompt} ({_workerCount.Value}/{maxWorkers})";
var coreResourceManager = CoreResourceManager.Instance;
if (coreResourceManager != null && !coreResourceManager.CanAfford(recruitmentCost))
return $"자원 부족 (필요: {recruitmentCost})";
return $"{interactionPrompt} ({_workerCount.Value}/{maxWorkers}) - 비용: {recruitmentCost}";
}
public string GetInteractionAnimation()