Add AI components and systems for enhanced enemy behavior and pathfinding
This commit is contained in:
54
GameCore/AI/AIAimSystem.cs
Normal file
54
GameCore/AI/AIAimSystem.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using GameCore.Combat;
|
||||||
|
using GameCore.ECS;
|
||||||
|
using GameCore.ECS.Interfaces;
|
||||||
|
using GameCore.Input;
|
||||||
|
using GameCore.Physics;
|
||||||
|
using GameCore.Player;
|
||||||
|
|
||||||
|
namespace GameCore.AI;
|
||||||
|
|
||||||
|
public class AIAimSystem : ISystem
|
||||||
|
{
|
||||||
|
private const float MinAimDistance = 0.5f;
|
||||||
|
|
||||||
|
public void Update(World world, float deltaTime)
|
||||||
|
{
|
||||||
|
var entities = world.GetEntitiesWith<AIComponent>();
|
||||||
|
foreach (var entity in entities)
|
||||||
|
{
|
||||||
|
if (world.GetComponent<PlayerComponent>(entity) != null)
|
||||||
|
{
|
||||||
|
world.Logger.Warn("AI entity has PlayerComponent; skipping AI aim processing.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ai = world.GetComponent<AIComponent>(entity);
|
||||||
|
var input = world.GetComponent<InputStateComponent>(entity);
|
||||||
|
var pos = world.GetComponent<PositionComponent>(entity);
|
||||||
|
var weapon = world.GetComponent<WeaponComponent>(entity);
|
||||||
|
|
||||||
|
if (ai == null || input == null || pos == null || weapon == null)
|
||||||
|
{
|
||||||
|
if (input != null) input.IsFiring = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.IsFiring = false;
|
||||||
|
|
||||||
|
if (ai.CurrentState == AIState.Chase || ai.CurrentState == AIState.Attack)
|
||||||
|
{
|
||||||
|
var vectorToTarget = ai.LastKnownTargetPosition - input.MuzzlePosition;
|
||||||
|
var distanceToTarget = vectorToTarget.Length();
|
||||||
|
|
||||||
|
if (distanceToTarget > MinAimDistance)
|
||||||
|
{
|
||||||
|
var directionToTarget = vectorToTarget.Normalize();
|
||||||
|
input.MuzzleDirection = directionToTarget;
|
||||||
|
input.LookDirection = directionToTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ai.CurrentState == AIState.Attack) input.IsFiring = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
GameCore/AI/AIComponent.cs
Normal file
20
GameCore/AI/AIComponent.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using GameCore.ECS;
|
||||||
|
using GameCore.ECS.Interfaces;
|
||||||
|
using GameCore.Math;
|
||||||
|
|
||||||
|
namespace GameCore.AI;
|
||||||
|
|
||||||
|
public class AIComponent : IComponent
|
||||||
|
{
|
||||||
|
public AIState CurrentState { get; set; } = AIState.Idle;
|
||||||
|
public Entity? Target { get; set; }
|
||||||
|
public float StateTimer { get; set; } = 0.0f;
|
||||||
|
public Vector3 LastKnownTargetPosition { get; set; } = Vector3.Zero;
|
||||||
|
|
||||||
|
public float SightRange { get; set; } = 20.0f;
|
||||||
|
public float FieldOfView { get; set; } = 90.0f; // in degrees
|
||||||
|
public float AttackRange { get; set; } = 10.0f;
|
||||||
|
public float ChaseGiveUpTime { get; set; } = 5.0f;
|
||||||
|
public float ReactionTime { get; set; } = 0.5f;
|
||||||
|
public float AttackRangeBufferPercent { get; set; } = 0.2f;
|
||||||
|
}
|
||||||
97
GameCore/AI/AIPathfindingSystem.cs
Normal file
97
GameCore/AI/AIPathfindingSystem.cs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
using GameCore.ECS;
|
||||||
|
using GameCore.ECS.Interfaces;
|
||||||
|
using GameCore.Input;
|
||||||
|
using GameCore.Math;
|
||||||
|
using GameCore.Physics;
|
||||||
|
using GameCore.Player;
|
||||||
|
|
||||||
|
namespace GameCore.AI;
|
||||||
|
|
||||||
|
public class AIPathfindingSystem : ISystem
|
||||||
|
{
|
||||||
|
private const float WaypointReachThreshold = 0.5f;
|
||||||
|
|
||||||
|
public void Update(World world, float deltaTime)
|
||||||
|
{
|
||||||
|
var entities = world.GetEntitiesWith<AIComponent>();
|
||||||
|
foreach (var entity in entities)
|
||||||
|
{
|
||||||
|
if (world.GetComponent<PlayerComponent>(entity) != null) continue;
|
||||||
|
|
||||||
|
var ai = world.GetComponent<AIComponent>(entity);
|
||||||
|
var input = world.GetComponent<InputStateComponent>(entity);
|
||||||
|
var pos = world.GetComponent<PositionComponent>(entity);
|
||||||
|
if (ai == null || input == null || pos == null) continue;
|
||||||
|
|
||||||
|
input.MoveDirection = Vector3.Zero;
|
||||||
|
Vector3? targetPosition = null;
|
||||||
|
|
||||||
|
switch (ai.CurrentState)
|
||||||
|
{
|
||||||
|
case AIState.Chase:
|
||||||
|
targetPosition = ai.LastKnownTargetPosition;
|
||||||
|
world.Logger.Debug($"AI Entity {entity.Id} chasing target at {targetPosition}");
|
||||||
|
break;
|
||||||
|
case AIState.Patrol:
|
||||||
|
var patrol = world.GetComponent<PatrolComponent>(entity);
|
||||||
|
if (patrol == null || patrol.PatrolPoints.Count == 0)
|
||||||
|
{
|
||||||
|
ai.CurrentState = AIState.Idle;
|
||||||
|
world.Logger.Debug($"AI Entity {entity.Id} has no patrol points, switching to Idle state.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPosition = patrol.PatrolPoints[patrol.CurrentPatrolIndex];
|
||||||
|
var horizontalDiff = new Vector3(targetPosition.Value.X - pos.Position.X, 0f,
|
||||||
|
targetPosition.Value.Z - pos.Position.Z);
|
||||||
|
var distanceToPatrolPoint = horizontalDiff.Length();
|
||||||
|
|
||||||
|
if (distanceToPatrolPoint < WaypointReachThreshold)
|
||||||
|
{
|
||||||
|
patrol.CurrentPatrolIndex++;
|
||||||
|
if (patrol.CurrentPatrolIndex >= patrol.PatrolPoints.Count)
|
||||||
|
patrol.CurrentPatrolIndex = patrol.IsLooping ? 0 : patrol.PatrolPoints.Count - 1;
|
||||||
|
targetPosition = patrol.PatrolPoints[patrol.CurrentPatrolIndex];
|
||||||
|
world.Logger.Debug(
|
||||||
|
$"AI Entity {entity.Id} reached patrol point, moving to next point at {targetPosition}");
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case AIState.Idle:
|
||||||
|
case AIState.Attack:
|
||||||
|
world.Logger.Debug($"AI Entity {entity.Id} is in state {ai.CurrentState}, not moving.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetPosition == null)
|
||||||
|
{
|
||||||
|
world.Logger.Debug($"AI Entity {entity.Id} has no target position.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = world.WorldQuery.GetPath(pos.Position, targetPosition.Value);
|
||||||
|
world.Logger.Debug($"Path for AI Entity {entity.Id}: {string.Join(" -> ", path)}");
|
||||||
|
|
||||||
|
if (path.Count > 0)
|
||||||
|
{
|
||||||
|
var nextWaypoint = path[0];
|
||||||
|
var horizontalDiffToWaypoint =
|
||||||
|
new Vector3(nextWaypoint.X - pos.Position.X, 0f, nextWaypoint.Z - pos.Position.Z);
|
||||||
|
if (horizontalDiffToWaypoint.Length() < 0.1f && path.Count > 1)
|
||||||
|
{
|
||||||
|
nextWaypoint = path[1];
|
||||||
|
world.Logger.Debug($"AI Entity {entity.Id} skipping first waypoint, moving to {nextWaypoint}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var directionVector = nextWaypoint - pos.Position;
|
||||||
|
input.MoveDirection = new Vector3(directionVector.X, 0f, directionVector.Z).Normalize();
|
||||||
|
world.Logger.Debug(
|
||||||
|
$"AI Entity {entity.Id} moving towards waypoint at {nextWaypoint} with direction {input.MoveDirection}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
world.Logger.Debug($"AI Entity {entity.Id} has no path.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
130
GameCore/AI/AIPerceptionSystem.cs
Normal file
130
GameCore/AI/AIPerceptionSystem.cs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
using GameCore.ECS;
|
||||||
|
using GameCore.ECS.Interfaces;
|
||||||
|
using GameCore.Math;
|
||||||
|
using GameCore.Movement;
|
||||||
|
using GameCore.Physics;
|
||||||
|
using GameCore.Player;
|
||||||
|
|
||||||
|
namespace GameCore.AI;
|
||||||
|
|
||||||
|
public class AIPerceptionSystem : ISystem
|
||||||
|
{
|
||||||
|
private readonly Vector3 _aiEyeOffset = new(0f, 1.5f, 0f);
|
||||||
|
private readonly Vector3 _playerCenterOffset = new(0f, 1.0f, 0f);
|
||||||
|
|
||||||
|
public void Update(World world, float deltaTime)
|
||||||
|
{
|
||||||
|
var playerEntities = world.GetEntitiesWith<PlayerComponent>().ToList();
|
||||||
|
if (!playerEntities.Any())
|
||||||
|
{
|
||||||
|
world.Logger.Warn("No player entity found for AI perception.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var playerEntity = playerEntities.First();
|
||||||
|
var playerPos = world.GetComponent<PositionComponent>(playerEntity);
|
||||||
|
if (playerPos == null)
|
||||||
|
{
|
||||||
|
world.Logger.Warn("No player position found for AI perception.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var aiEntities = world.GetEntitiesWith<AIComponent>();
|
||||||
|
foreach (var entity in aiEntities)
|
||||||
|
{
|
||||||
|
if (world.GetComponent<PlayerComponent>(entity) != null)
|
||||||
|
{
|
||||||
|
world.Logger.Warn("AI Perception System: Skipping player entity.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ai = world.GetComponent<AIComponent>(entity);
|
||||||
|
var aiPos = world.GetComponent<PositionComponent>(entity);
|
||||||
|
var aiRot = world.GetComponent<RotationComponent>(entity);
|
||||||
|
if (ai == null || aiPos == null || aiRot == null)
|
||||||
|
{
|
||||||
|
world.Logger.Warn("AI Perception System: Skipping AI perception. Missing components.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ai.StateTimer += deltaTime;
|
||||||
|
|
||||||
|
var aiEyePos = aiPos.Position + _aiEyeOffset;
|
||||||
|
var playerCenterPos = playerPos.Position + _playerCenterOffset;
|
||||||
|
|
||||||
|
var vectorToPlayer = playerCenterPos - aiEyePos;
|
||||||
|
var distanceToPlayer = vectorToPlayer.Length();
|
||||||
|
var directionToPlayer = vectorToPlayer.Normalize();
|
||||||
|
|
||||||
|
if (distanceToPlayer < ai.SightRange &&
|
||||||
|
IsInFieldOfView(world, ai, aiRot, directionToPlayer) &&
|
||||||
|
HasLineOfSight(world, aiEyePos, playerCenterPos, entity))
|
||||||
|
{
|
||||||
|
ai.Target = playerEntity;
|
||||||
|
ai.LastKnownTargetPosition = playerPos.Position;
|
||||||
|
ai.StateTimer = 0.0f;
|
||||||
|
|
||||||
|
var attackRangeBuffer = ai.AttackRange * ai.AttackRangeBufferPercent;
|
||||||
|
if (distanceToPlayer <= ai.AttackRange)
|
||||||
|
{
|
||||||
|
if (ai.CurrentState != AIState.Attack)
|
||||||
|
{
|
||||||
|
ai.CurrentState = AIState.Attack;
|
||||||
|
world.Logger.Debug($"AI Entity {entity.Id} switching to Attack state.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (distanceToPlayer > ai.AttackRange + attackRangeBuffer)
|
||||||
|
{
|
||||||
|
if (ai.CurrentState != AIState.Chase)
|
||||||
|
{
|
||||||
|
ai.CurrentState = AIState.Chase;
|
||||||
|
world.Logger.Debug($"AI Entity {entity.Id} switching to Chase state.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ai.Target != null)
|
||||||
|
{
|
||||||
|
if (ai.StateTimer > ai.ChaseGiveUpTime)
|
||||||
|
{
|
||||||
|
ai.Target = null;
|
||||||
|
ai.CurrentState = world.GetComponent<PatrolComponent>(entity) != null
|
||||||
|
? AIState.Patrol
|
||||||
|
: AIState.Idle;
|
||||||
|
world.Logger.Debug($"AI Entity {entity.Id} lost target, switching to {ai.CurrentState} state.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ai.CurrentState = AIState.Chase;
|
||||||
|
world.Logger.Debug($"AI Entity {entity.Id} continuing to chase last known target position.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ai.CurrentState = world.GetComponent<PatrolComponent>(entity) != null
|
||||||
|
? AIState.Patrol
|
||||||
|
: AIState.Idle;
|
||||||
|
world.Logger.Debug($"AI Entity {entity.Id} has no target, switching to {ai.CurrentState} state.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasLineOfSight(World world, Vector3 from, Vector3 to, Entity owner)
|
||||||
|
{
|
||||||
|
var hit = world.WorldQuery.Raycast(from, to, owner);
|
||||||
|
return hit.DidHit && hit.HitEntity.HasValue && world.GetComponent<PlayerComponent>(hit.HitEntity.Value) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsInFieldOfView(World world, AIComponent ai, RotationComponent aiRot, Vector3 directionToPlayer)
|
||||||
|
{
|
||||||
|
var aiForward = world.WorldQuery.RotateVectorByYaw(new Vector3(0f, 0f, 1f), aiRot.Rotation.Y);
|
||||||
|
|
||||||
|
var dotProduct = Vector3.Dot(aiForward, directionToPlayer);
|
||||||
|
var fovRadians = ai.FieldOfView * (float)System.Math.PI / 180f;
|
||||||
|
var fovThreshold = (float)System.Math.Cos(fovRadians / 2f);
|
||||||
|
|
||||||
|
return dotProduct >= fovThreshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
GameCore/AI/AIState.cs
Normal file
9
GameCore/AI/AIState.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace GameCore.AI;
|
||||||
|
|
||||||
|
public enum AIState
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
Patrol,
|
||||||
|
Chase,
|
||||||
|
Attack
|
||||||
|
}
|
||||||
11
GameCore/AI/PatrolComponent.cs
Normal file
11
GameCore/AI/PatrolComponent.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using GameCore.ECS.Interfaces;
|
||||||
|
using GameCore.Math;
|
||||||
|
|
||||||
|
namespace GameCore.AI;
|
||||||
|
|
||||||
|
public class PatrolComponent : IComponent
|
||||||
|
{
|
||||||
|
public List<Vector3> PatrolPoints { get; set; } = [];
|
||||||
|
public int CurrentPatrolIndex { get; set; } = 0;
|
||||||
|
public bool IsLooping { get; set; } = false;
|
||||||
|
}
|
||||||
@@ -7,4 +7,5 @@ public interface IWorldQuery
|
|||||||
HitResult Raycast(Vector3 from, Vector3 to, Entity? ownerToExclude = null);
|
HitResult Raycast(Vector3 from, Vector3 to, Entity? ownerToExclude = null);
|
||||||
Vector3 RotateVectorByYaw(Vector3 vector, float yawRadians);
|
Vector3 RotateVectorByYaw(Vector3 vector, float yawRadians);
|
||||||
IEnumerable<Entity> OverlapSphere(Vector3 position, float radius, Entity? ownerToExclude = null);
|
IEnumerable<Entity> OverlapSphere(Vector3 position, float radius, Entity? ownerToExclude = null);
|
||||||
|
List<Vector3> GetPath(Vector3 start, Vector3 end);
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using GameCore.AI;
|
||||||
using GameCore.ECS;
|
using GameCore.ECS;
|
||||||
using GameCore.ECS.Interfaces;
|
using GameCore.ECS.Interfaces;
|
||||||
using GameCore.Player;
|
using GameCore.Player;
|
||||||
@@ -8,12 +9,26 @@ public class PlayerInputSystem : ISystem
|
|||||||
{
|
{
|
||||||
public void Update(World world, float deltaTime)
|
public void Update(World world, float deltaTime)
|
||||||
{
|
{
|
||||||
var playerEntities = world.GetEntitiesWith<PlayerComponent>();
|
var playerEntities = world.GetEntitiesWith<PlayerComponent>().ToList();
|
||||||
if (!playerEntities.Any()) return;
|
if (!playerEntities.Any())
|
||||||
|
{
|
||||||
|
world.Logger.Warn("No player entity found for input processing.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var playerEntity = playerEntities.First();
|
var playerEntity = playerEntities.First();
|
||||||
|
if (world.GetComponent<AIComponent>(playerEntity) != null)
|
||||||
|
{
|
||||||
|
world.Logger.Warn("Player entity has AIComponent; skipping input processing.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var inputState = world.GetComponent<InputStateComponent>(playerEntity);
|
var inputState = world.GetComponent<InputStateComponent>(playerEntity);
|
||||||
if (inputState == null) return;
|
if (inputState == null)
|
||||||
|
{
|
||||||
|
world.Logger.Warn("Player entity is missing InputStateComponent; cannot process input.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
inputState.MoveDirection = world.InputService.MoveDirection.Normalize();
|
inputState.MoveDirection = world.InputService.MoveDirection.Normalize();
|
||||||
inputState.LookDirection = world.InputService.LookDirection;
|
inputState.LookDirection = world.InputService.LookDirection;
|
||||||
|
|||||||
@@ -76,4 +76,9 @@ public struct Vector3
|
|||||||
|
|
||||||
return new Vector3(X / length, Y / length, Z / length);
|
return new Vector3(X / length, Y / length, Z / length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static float Dot(Vector3 a, Vector3 b)
|
||||||
|
{
|
||||||
|
return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,14 +8,17 @@ public class GravitySystem : ISystem
|
|||||||
{
|
{
|
||||||
public void Update(World world, float deltaTime)
|
public void Update(World world, float deltaTime)
|
||||||
{
|
{
|
||||||
var entities = world.GetEntitiesWith<VelocityComponent>();
|
var entities = world.GetEntitiesWith<CharacterStateComponent>();
|
||||||
foreach (var entity in entities)
|
foreach (var entity in entities)
|
||||||
{
|
{
|
||||||
var velocity = world.GetComponent<VelocityComponent>(entity);
|
var velocity = world.GetComponent<VelocityComponent>(entity);
|
||||||
var characterState = world.GetComponent<CharacterStateComponent>(entity);
|
var characterState = world.GetComponent<CharacterStateComponent>(entity);
|
||||||
|
|
||||||
if (velocity == null || characterState == null)
|
if (velocity == null || characterState == null)
|
||||||
|
{
|
||||||
|
world.Logger.Warn("GravitySystem: Missing VelocityComponent or CharacterStateComponent.");
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!characterState.IsOnFloor) velocity.DesiredVelocity.Y -= world.Config.GravityStrength * deltaTime;
|
if (!characterState.IsOnFloor) velocity.DesiredVelocity.Y -= world.Config.GravityStrength * deltaTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using GameCore.ECS;
|
using GameCore.ECS;
|
||||||
using GameCore.ECS.Interfaces;
|
using GameCore.ECS.Interfaces;
|
||||||
using GameCore.Input;
|
using GameCore.Input;
|
||||||
|
using GameCore.Player;
|
||||||
|
|
||||||
namespace GameCore.Movement;
|
namespace GameCore.Movement;
|
||||||
|
|
||||||
@@ -18,10 +19,28 @@ public class RotationSystem : ISystem
|
|||||||
var rotation = world.GetComponent<RotationComponent>(entity);
|
var rotation = world.GetComponent<RotationComponent>(entity);
|
||||||
|
|
||||||
if (input == null || rotation == null)
|
if (input == null || rotation == null)
|
||||||
|
{
|
||||||
|
world.Logger.Warn("RotationSystem: Missing InputStateComponent or RotationComponent.");
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (world.GetComponent<PlayerComponent>(entity) != null)
|
||||||
|
{
|
||||||
|
rotation.Rotation.Y += input.LookDirection.Y;
|
||||||
|
rotation.Rotation.X += input.LookDirection.X;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (input.LookDirection.Length() > 0.001f)
|
||||||
|
{
|
||||||
|
var lookDir = input.LookDirection;
|
||||||
|
rotation.Rotation.Y = (float)System.Math.Atan2(lookDir.X, lookDir.Z);
|
||||||
|
|
||||||
|
var horizontalDist = (float)System.Math.Sqrt(lookDir.X * lookDir.X + lookDir.Z * lookDir.Z);
|
||||||
|
rotation.Rotation.X = -(float)System.Math.Atan2(lookDir.Y, horizontalDist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rotation.Rotation.Y += input.LookDirection.Y;
|
|
||||||
rotation.Rotation.X += input.LookDirection.X;
|
|
||||||
|
|
||||||
rotation.Rotation.X = System.Math.Clamp(rotation.Rotation.X, MinPitch, MaxPitch);
|
rotation.Rotation.X = System.Math.Clamp(rotation.Rotation.X, MinPitch, MaxPitch);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user