feat: 투사체 발사 스킬 구현

- SkillProjectile: 서버 권위 이동/충돌, caster 자식 콜라이더 충돌 무시 추가
- SpawnEffect: hitEffect 필드 추가 (투사체 명중 시 적용할 효과 분리)
- SkillEffect: Team 컴포넌트 없는 환경 오브젝트 타겟 제외 처리
- Prefab_Skill_ProjectileBasic 프리팹 생성 (NetworkObject + NetworkTransform + Rigidbody + SphereCollider)
- 투사체 스킬 에셋 추가 (SkillData, SpawnEffect, DamageEffect)
- Anim_Common_찌르기 애니메이션 이벤트 추가 (OnEffect @ 0.867s, OnSkillEnd @ 1.3s)
- DefaultNetworkPrefabs에 Prefab_Skill_ProjectileBasic 등록

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-18 15:51:41 +09:00
co-authored by Claude Sonnet 4.6
parent f73a2ca9d7
commit a9aa3a3091
16 changed files with 2626 additions and 18 deletions
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 94f0a76cebcac2f4fb5daf1b675fd79f, type: 3}
m_Name: "Data_Skill_Player_\uD22C\uC0AC\uCCB4"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillData
skillName: "\uD22C\uC0AC\uCCB4"
description: "\uD22C\uC0AC\uCCB4\uB97C \uBC1C\uC0AC\uD569\uB2C8\uB2E4. \uBA85\uC911
\uC2DC \uC6D0\uAC70\uB9AC \uB300\uBBF8\uC9C0\uB97C \uC785\uD799\uB2C8\uB2E4."
icon: {fileID: 0}
skillClip: {fileID: -8689311932429934276, guid: ac0adc4c7f982fe4d82eac9c2267f0c6, type: 3}
endClip: {fileID: 0}
useRootMotion: 1
ignoreRootMotionY: 1
cooldown: 1.5
manaCost: 10
effects:
- {fileID: 11400000, guid: fa684d4a7ce68d54aa4ce101f9400c35, type: 2}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b8c86399865e91144a3d6fcfddc04fd9
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,28 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 58efb3c775496fa40b801b21127a011e, type: 3}
m_Name: "Data_SkillEffect_Player_\uD22C\uC0AC\uCCB4_0_\uB370\uBBF8\uC9C0"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.DamageEffect
targetType: 0
targetTeam: 0
areaCenter: 0
areaShape: 0
targetLayers:
serializedVersion: 2
m_Bits: 0
areaRadius: 3
fanOriginDistance: 1
fanRadius: 3
fanHalfAngle: 45
baseDamage: 15
damageType: 2
statScaling: 1
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: afcf53efc3a0ba843a0cd3aca8922542
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,31 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a3139ddf07cfe324fa692a88cd565e24, type: 3}
m_Name: "Data_SkillEffect_Player_\uD22C\uC0AC\uCCB4_1_\uC2A4\uD3F0"
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.SpawnEffect
targetType: 0
targetTeam: 0
areaCenter: 0
areaShape: 0
targetLayers:
serializedVersion: 2
m_Bits: 0
areaRadius: 3
fanOriginDistance: 1
fanRadius: 3
fanHalfAngle: 45
prefab: {fileID: 7991191450305394598, guid: b8e3d022f0a2ce84da42fe4afd4a1b13, type: 3}
spawnLocation: 1
spawnOffset: {x: 0, y: 1, z: 0}
parentToCaster: 0
autoDestroyTime: 0
hitEffect: {fileID: 11400000, guid: afcf53efc3a0ba843a0cd3aca8922542, type: 2}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fa684d4a7ce68d54aa4ce101f9400c35
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -295,7 +295,8 @@ MonoBehaviour:
animator: {fileID: 3426985706796420257}
baseController: {fileID: 9100000, guid: db718381bb2992e469c76c64015e065b, type: 2}
baseSkillClip: {fileID: -7717634560727564301, guid: 0f6fd9302e489b94d96774e2713b1317, type: 3}
registeredClips: []
registeredClips:
- {fileID: -8689311932429934276, guid: ac0adc4c7f982fe4d82eac9c2267f0c6, type: 3}
debugMode: 1
showAreaDebug: 1
debugDrawDuration: 1
@@ -314,7 +315,7 @@ MonoBehaviour:
ShowTopMostFoldoutHeaderGroup: 1
skillSlots:
- {fileID: 11400000, guid: b7f09e0e899c8fc4bb2cc9204cc6eb4a, type: 2}
- {fileID: 0}
- {fileID: 11400000, guid: b8c86399865e91144a3d6fcfddc04fd9, type: 2}
- {fileID: 0}
- {fileID: 0}
- {fileID: 0}
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2d4602d75b46da74dab668697376ee59
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,235 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &7991191450305394598
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3361050264540242122}
- component: {fileID: 3444955663463127284}
- component: {fileID: 8311218787009392355}
- component: {fileID: 7074768914716488529}
- component: {fileID: 6175091974655926604}
- component: {fileID: 217346389058944041}
- component: {fileID: 7983858810247897309}
- component: {fileID: 238577235165339599}
m_Layer: 0
m_Name: Projectile_Basic
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &3361050264540242122
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7991191450305394598}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.3, y: 0.3, z: 0.3}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &3444955663463127284
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7991191450305394598}
m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0}
--- !u!135 &8311218787009392355
SphereCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7991191450305394598}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 1
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
m_Radius: 0.5
m_Center: {x: 0, y: 0, z: 0}
--- !u!23 &7074768914716488529
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7991191450305394598}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 1
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!114 &6175091974655926604
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7991191450305394598}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
GlobalObjectIdHash: 897395779
InScenePlacedSourceGlobalObjectIdHash: 0
DeferredDespawnTick: 0
Ownership: 1
AlwaysReplicateAsRoot: 0
SynchronizeTransform: 1
ActiveSceneSynchronization: 0
SceneMigrationSynchronization: 0
SpawnWithObservers: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
SyncOwnerTransformWhenParented: 1
AllowOwnerToParent: 0
--- !u!114 &217346389058944041
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7991191450305394598}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e96cb6065543e43c4a752faaa1468eb1, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.Components.NetworkTransform
ShowTopMostFoldoutHeaderGroup: 1
NetworkTransformExpanded: 0
AutoOwnerAuthorityTickOffset: 1
PositionInterpolationType: 0
RotationInterpolationType: 0
ScaleInterpolationType: 0
PositionLerpSmoothing: 1
PositionMaxInterpolationTime: 0.1
RotationLerpSmoothing: 1
RotationMaxInterpolationTime: 0.1
ScaleLerpSmoothing: 1
ScaleMaxInterpolationTime: 0.1
AuthorityMode: 0
TickSyncChildren: 0
UseUnreliableDeltas: 0
SyncPositionX: 1
SyncPositionY: 1
SyncPositionZ: 1
SyncRotAngleX: 1
SyncRotAngleY: 1
SyncRotAngleZ: 1
SyncScaleX: 1
SyncScaleY: 1
SyncScaleZ: 1
PositionThreshold: 0.001
RotAngleThreshold: 0.01
ScaleThreshold: 0.01
UseQuaternionSynchronization: 0
UseQuaternionCompression: 0
UseHalfFloatPrecision: 0
InLocalSpace: 0
SwitchTransformSpaceWhenParented: 0
Interpolate: 1
SlerpPosition: 0
--- !u!54 &7983858810247897309
Rigidbody:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7991191450305394598}
serializedVersion: 5
m_Mass: 1
m_LinearDamping: 0
m_AngularDamping: 0.05
m_CenterOfMass: {x: 0, y: 0, z: 0}
m_InertiaTensor: {x: 1, y: 1, z: 1}
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_ImplicitCom: 1
m_ImplicitTensor: 1
m_UseGravity: 1
m_IsKinematic: 0
m_Interpolate: 0
m_Constraints: 0
m_CollisionDetection: 0
--- !u!114 &238577235165339599
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7991191450305394598}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6e2a203fbf7bc39449b13bec67e94150, type: 3}
m_Name:
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.SkillProjectile
ShowTopMostFoldoutHeaderGroup: 1
speed: 15
lifetime: 5
penetrate: 0
maxPenetration: 1
hitEffect: {fileID: 0}
hitEffectDuration: 2
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b8e3d022f0a2ce84da42fe4afd4a1b13
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,4 +1,5 @@
using UnityEngine;
using Unity.Netcode;
namespace Colosseum.Skills.Effects
{
@@ -15,26 +16,42 @@ namespace Colosseum.Skills.Effects
[SerializeField] private bool parentToCaster = false;
[Min(0f)] [SerializeField] private float autoDestroyTime = 3f;
[Header("Hit Settings")]
[Tooltip("투사체가 대상에 명중했을 때 적용할 효과. 미설정 시 명중 효과 없음.")]
[SerializeField] private SkillEffect hitEffect;
protected override void ApplyEffect(GameObject caster, GameObject target)
{
if (prefab == null || caster == null) return;
Vector3 spawnPos = GetSpawnPosition(caster, target) + spawnOffset;
Quaternion spawnRot = GetSpawnRotation(caster, target);
Transform parent = parentToCaster ? caster.transform : null;
GameObject instance = Object.Instantiate(prefab, spawnPos, spawnRot, parent);
// SkillProjectile 컴포넌트가 있으면 초기화
var projectile = instance.GetComponent<SkillProjectile>();
if (projectile != null)
var networkObject = prefab.GetComponent<NetworkObject>();
if (networkObject != null)
{
projectile.Initialize(caster, this);
// 네트워크 오브젝트: 서버에서 스폰 후 전파
// (OnEffect 가드에 의해 이미 서버에서만 호출됨)
GameObject instance = Object.Instantiate(prefab, spawnPos, spawnRot);
var spawnedNet = instance.GetComponent<NetworkObject>();
spawnedNet.Spawn(destroyWithScene: true);
var projectile = instance.GetComponent<SkillProjectile>();
if (projectile != null)
projectile.Initialize(caster, hitEffect);
}
if (autoDestroyTime > 0f)
else
{
Object.Destroy(instance, autoDestroyTime);
// 로컬 오브젝트 (파티클 등): 기존 방식 유지
Transform parent = parentToCaster ? caster.transform : null;
GameObject instance = Object.Instantiate(prefab, spawnPos, spawnRot, parent);
var projectile = instance.GetComponent<SkillProjectile>();
if (projectile != null)
projectile.Initialize(caster, hitEffect);
if (autoDestroyTime > 0f)
Object.Destroy(instance, autoDestroyTime);
}
}
@@ -93,6 +93,9 @@ namespace Colosseum.Skills
private bool IsCorrectTeam(GameObject caster, GameObject target)
{
// Team 컴포넌트가 없는 오브젝트(환경, 바닥, 벽 등)는 타겟 제외
if (target.GetComponent<Team>() == null) return false;
bool isSameTeam = Team.IsSameTeam(caster, target);
return targetTeam switch
+24 -6
View File
@@ -1,12 +1,14 @@
using UnityEngine;
using Unity.Netcode;
namespace Colosseum.Skills
{
/// <summary>
/// 스킬 투사체. 충돌 시 연결된 효과를 적용합니다.
/// 서버에서만 이동/충돌을 처리하며, NetworkTransform으로 위치를 동기화합니다.
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class SkillProjectile : MonoBehaviour
public class SkillProjectile : NetworkBehaviour
{
[Header("이동 설정")]
[Min(0f)] [SerializeField] private float speed = 15f;
@@ -41,29 +43,39 @@ namespace Colosseum.Skills
rb.useGravity = false;
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
}
// caster 및 자식 콜라이더와의 충돌 무시
var myColliders = GetComponents<Collider>();
foreach (var cc in caster.GetComponentsInChildren<Collider>())
foreach (var mc in myColliders)
Physics.IgnoreCollision(mc, cc);
}
private void Start()
{
Destroy(gameObject, lifetime);
// 서버에서만 수명 관리
if (IsServer)
Invoke(nameof(ServerDespawn), lifetime);
}
private void FixedUpdate()
{
if (!initialized || rb == null) return;
// 서버에서만 이동 처리
if (!IsServer || !initialized || rb == null) return;
rb.linearVelocity = transform.forward * speed;
}
private void OnTriggerEnter(Collider other)
{
if (!initialized || sourceEffect == null) return;
// 서버에서만 충돌 처리
if (!IsServer || !initialized || sourceEffect == null) return;
if (other.gameObject == caster) return;
// 유효한 타겟인지 확인
if (!sourceEffect.IsValidTarget(caster, other.gameObject))
return;
// 충돌 이펙트
// 충돌 이펙트 (서버에서 스폰 → 클라이언트에도 표시되려면 NetworkObject여야 함)
if (hitEffect != null)
{
var effect = Instantiate(hitEffect, transform.position, transform.rotation);
@@ -77,10 +89,16 @@ namespace Colosseum.Skills
if (!penetrate || penetrationCount >= maxPenetration)
{
Destroy(gameObject);
ServerDespawn();
}
}
private void ServerDespawn()
{
if (!IsServer || !IsSpawned) return;
NetworkObject.Despawn(true);
}
public void SetDirection(Vector3 direction)
{
transform.rotation = Quaternion.LookRotation(direction.normalized);