Initial Commit

This commit is contained in:
2026-03-16 14:38:46 +02:00
commit b8f7327a21
2327 changed files with 253610 additions and 0 deletions
@@ -0,0 +1,10 @@
using System;
namespace Zenject.SpaceFighter
{
[Serializable]
public class EnemyCommonSettings
{
public float AttackDistance = 15.0f;
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 28bfa2b5275427041ae8782a2fa41625
timeCreated: 1484712200
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,50 @@
using System;
using UnityEngine;
namespace Zenject.SpaceFighter
{
public class EnemyDeathHandler
{
readonly EnemyFacade _facade;
readonly SignalBus _signalBus;
readonly Settings _settings;
readonly Explosion.Factory _explosionFactory;
readonly AudioPlayer _audioPlayer;
readonly EnemyView _view;
public EnemyDeathHandler(
EnemyView view,
AudioPlayer audioPlayer,
Explosion.Factory explosionFactory,
Settings settings,
SignalBus signalBus,
EnemyFacade facade)
{
_facade = facade;
_signalBus = signalBus;
_settings = settings;
_explosionFactory = explosionFactory;
_audioPlayer = audioPlayer;
_view = view;
}
public void Die()
{
var explosion = _explosionFactory.Create();
explosion.transform.position = _view.Position;
_audioPlayer.Play(_settings.DeathSound, _settings.DeathSoundVolume);
_signalBus.Fire<EnemyKilledSignal>();
_facade.Dispose();
}
[Serializable]
public class Settings
{
public AudioClip DeathSound;
public float DeathSoundVolume = 1.0f;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 52b884b41e3642c489f58f461470f589
timeCreated: 1505656096
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,82 @@
using System;
using UnityEngine;
namespace Zenject.SpaceFighter
{
// Here we can add some high-level methods to give some info to other
// parts of the codebase outside of our enemy facade
public class EnemyFacade : MonoBehaviour, IPoolable<float, float, IMemoryPool>, IDisposable
{
EnemyView _view;
EnemyTunables _tunables;
EnemyDeathHandler _deathHandler;
EnemyStateManager _stateManager;
EnemyRegistry _registry;
IMemoryPool _pool;
[Inject]
public void Construct(
EnemyView view,
EnemyTunables tunables,
EnemyDeathHandler deathHandler,
EnemyStateManager stateManager,
EnemyRegistry registry)
{
_view = view;
_tunables = tunables;
_deathHandler = deathHandler;
_stateManager = stateManager;
_registry = registry;
}
public EnemyStates State
{
get { return _stateManager.CurrentState; }
}
public float Accuracy
{
get { return _tunables.Accuracy; }
}
public float Speed
{
get { return _tunables.Speed; }
}
public Vector3 Position
{
get { return _view.Position; }
set { _view.Position = value; }
}
public void Dispose()
{
_pool.Despawn(this);
}
public void Die()
{
_deathHandler.Die();
}
public void OnDespawned()
{
_registry.RemoveEnemy(this);
_pool = null;
}
public void OnSpawned(float accuracy, float speed, IMemoryPool pool)
{
_pool = pool;
_tunables.Accuracy = accuracy;
_tunables.Speed = speed;
_registry.AddEnemy(this);
}
public class Factory : PlaceholderFactory<float, float, EnemyFacade>
{
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: fac47e4da43b2ca4d955370e0dc6990e
timeCreated: 1456415139
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace Zenject.SpaceFighter
{
public class EnemyRegistry
{
readonly List<EnemyFacade> _enemies = new List<EnemyFacade>();
public IEnumerable<EnemyFacade> Enemies
{
get { return _enemies; }
}
public void AddEnemy(EnemyFacade enemy)
{
_enemies.Add(enemy);
}
public void RemoveEnemy(EnemyFacade enemy)
{
_enemies.Remove(enemy);
}
}
}
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 401e2ff37163f8f428397277aba9b3a0
timeCreated: 1528644922
licenseType: Free
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,47 @@
using System;
using UnityEngine;
namespace Zenject.SpaceFighter
{
// Responsibilities:
// - Interpolate rotation of enemy towards the current desired
// direction
public class EnemyRotationHandler : IFixedTickable
{
readonly Settings _settings;
readonly EnemyView _view;
public EnemyRotationHandler(
EnemyView view,
Settings settings)
{
_settings = settings;
_view = view;
}
public Vector2 DesiredLookDir
{
get; set;
}
public void FixedTick()
{
var lookDir = _view.LookDir;
var error = Vector3.Angle(lookDir, DesiredLookDir);
if (Vector3.Cross(lookDir, DesiredLookDir).z < 0)
{
error *= -1;
}
_view.AddTorque(error * _settings.TurnSpeed);
}
[Serializable]
public class Settings
{
public float TurnSpeed;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 55eb8b68455f215489114a3a9a6f976d
timeCreated: 1456672456
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,98 @@
using System.Collections.Generic;
using ModestTree;
using UnityEngine;
namespace Zenject.SpaceFighter
{
public interface IEnemyState
{
void EnterState();
void ExitState();
void Update();
void FixedUpdate();
}
public enum EnemyStates
{
Idle,
Attack,
Follow,
None
}
// This class controls the basic "AI" of our enemy
// Which works as a finite state machine, between three states:
// - Attack
// - Follow/Chase
// - Idle
public class EnemyStateManager : ITickable, IFixedTickable, IInitializable
{
IEnemyState _currentStateHandler;
EnemyStates _currentState = EnemyStates.None;
EnemyView _view;
List<IEnemyState> _states;
// We can't use a constructor due to a circular dependency issue
[Inject]
public void Construct(
EnemyView view,
EnemyStateIdle idle, EnemyStateAttack attack, EnemyStateFollow follow)
{
_view = view;
_states = new List<IEnemyState>
{
// This needs to follow the enum order
idle, attack, follow
};
}
public EnemyStates CurrentState
{
get { return _currentState; }
}
public void Initialize()
{
Assert.IsEqual(_currentState, EnemyStates.None);
Assert.IsNull(_currentStateHandler);
ChangeState(EnemyStates.Follow);
}
public void ChangeState(EnemyStates state)
{
if (_currentState == state)
{
// Already in state
return;
}
//Log.Trace("View Changing state from {0} to {1}", _currentState, state);
_currentState = state;
if (_currentStateHandler != null)
{
_currentStateHandler.ExitState();
_currentStateHandler = null;
}
_currentStateHandler = _states[(int)state];
_currentStateHandler.EnterState();
}
public void Tick()
{
// Always ensure we are on the main plane
_view.Position = new Vector3(_view.Position.x, _view.Position.y, 0);
_currentStateHandler.Update();
}
public void FixedTick()
{
_currentStateHandler.FixedUpdate();
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 4087d354966adf44fa24ee0fa597f6da
timeCreated: 1456617036
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,13 @@
using System;
namespace Zenject.SpaceFighter
{
// These values are given as parameters into dynamically created
// EnemyFacade instances
[Serializable]
public class EnemyTunables
{
public float Accuracy;
public float Speed;
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: ff5c7e92da51264499aca355ed207cbd
timeCreated: 1456626929
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,85 @@
using UnityEngine;
namespace Zenject.SpaceFighter
{
public class EnemyView : MonoBehaviour
{
[SerializeField]
MeshRenderer _renderer = null;
[SerializeField]
Collider _collider = null;
[SerializeField]
Rigidbody _rigidBody = null;
[Inject]
public EnemyFacade Facade
{
get; set;
}
public MeshRenderer Renderer
{
get { return _renderer; }
}
public Collider Collider
{
get { return _collider; }
}
public Rigidbody Rigidbody
{
get { return _rigidBody; }
}
public Vector3 LookDir
{
get { return -_rigidBody.transform.right; }
}
public Vector3 RightDir
{
get { return _rigidBody.transform.up; }
}
public Vector3 ForwardDir
{
get { return _rigidBody.transform.right; }
}
public Vector3 Position
{
get { return _rigidBody.transform.position; }
set { _rigidBody.transform.position = value; }
}
public Quaternion Rotation
{
get { return _rigidBody.rotation; }
set { _rigidBody.rotation = value; }
}
public Vector3 Velocity
{
get { return _rigidBody.linearVelocity; }
}
public Vector3 AngularVelocity
{
get { return _rigidBody.angularVelocity; }
set { _rigidBody.angularVelocity = value; }
}
public void AddForce(Vector3 force)
{
_rigidBody.AddForce(force);
}
public void AddTorque(float value)
{
_rigidBody.AddTorque(Vector3.forward * value);
}
}
}
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: ae49206d36b59354b98f680504160bc4
timeCreated: 1528640620
licenseType: Free
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,10 @@
namespace Zenject.SpaceFighter
{
public struct PlayerDiedSignal
{
}
public struct EnemyKilledSignal
{
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 432f74bf3abdd7e4facd4bba2a8fe60b
timeCreated: 1483820546
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: b1c94638b93aa2646a2643e39a156dd9
folderAsset: yes
timeCreated: 1456618194
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,137 @@
using System;
using UnityEngine;
using Random = UnityEngine.Random;
namespace Zenject.SpaceFighter
{
public class EnemyStateAttack : IEnemyState
{
readonly EnemyRotationHandler _rotationHandler;
readonly EnemyCommonSettings _commonSettings;
readonly AudioPlayer _audioPlayer;
readonly EnemyTunables _tunables;
readonly EnemyStateManager _stateManager;
readonly PlayerFacade _player;
readonly Settings _settings;
readonly EnemyView _view;
readonly Bullet.Factory _bulletFactory;
float _lastShootTime;
bool _strafeRight;
float _lastStrafeChangeTime;
public EnemyStateAttack(
Bullet.Factory bulletFactory,
EnemyView view,
Settings settings,
PlayerFacade player,
EnemyStateManager stateManager,
EnemyTunables tunables,
AudioPlayer audioPlayer,
EnemyCommonSettings commonSettings,
EnemyRotationHandler rotationHandler)
{
_rotationHandler = rotationHandler;
_commonSettings = commonSettings;
_audioPlayer = audioPlayer;
_tunables = tunables;
_stateManager = stateManager;
_player = player;
_settings = settings;
_view = view;
_bulletFactory = bulletFactory;
_strafeRight = Random.Range(0.0f, 1.0f) < 0.5f;
}
public void EnterState()
{
}
public void ExitState()
{
}
public void Update()
{
if (_player.IsDead)
{
_stateManager.ChangeState(EnemyStates.Idle);
return;
}
_rotationHandler.DesiredLookDir = (_player.Position - _view.Position).normalized;
// Strafe back and forth over the given interval
if (Time.realtimeSinceStartup - _lastStrafeChangeTime > _settings.StrafeChangeInterval)
{
_lastStrafeChangeTime = Time.realtimeSinceStartup;
_strafeRight = !_strafeRight;
}
// Shoot every X seconds
if (Time.realtimeSinceStartup - _lastShootTime > _settings.ShootInterval)
{
_lastShootTime = Time.realtimeSinceStartup;
Fire();
}
// If the player runs away then chase them
if ((_player.Position - _view.Position).magnitude > _commonSettings.AttackDistance + _settings.AttackRangeBuffer)
{
_stateManager.ChangeState(EnemyStates.Follow);
}
}
public void FixedUpdate()
{
// Strafe to avoid getting hit too easily
if (_strafeRight)
{
_view.AddForce(_view.RightDir * _settings.StrafeMultiplier * _tunables.Speed);
}
else
{
_view.AddForce(-_view.RightDir * _settings.StrafeMultiplier * _tunables.Speed);
}
}
void Fire()
{
var bullet = _bulletFactory.Create(
_settings.BulletSpeed, _settings.BulletLifetime, BulletTypes.FromEnemy);
// Randomize our aim a bit
var accuracy = Mathf.Clamp(_tunables.Accuracy, 0, 1);
var maxError = 1.0f - accuracy;
var error = Random.Range(0, maxError);
if (Random.Range(0.0f, 1.0f) < 0.5f)
{
error *= -1;
}
var thetaError = error * _settings.ErrorRangeTheta;
bullet.transform.position = _view.Position + _view.LookDir * _settings.BulletOffsetDistance;
bullet.transform.rotation = Quaternion.AngleAxis(thetaError, Vector3.forward) * _view.Rotation;
_audioPlayer.Play(_settings.ShootSound, _settings.ShootSoundVolume);
}
[Serializable]
public class Settings
{
public AudioClip ShootSound;
public float ShootSoundVolume = 1.0f;
public float BulletLifetime;
public float BulletSpeed;
public float BulletOffsetDistance;
public float ShootInterval;
public float ErrorRangeTheta;
public float AttackRangeBuffer;
public float StrafeMultiplier;
public float StrafeChangeInterval;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 3d2bd141d86c3fb40a0a616c19e6de0c
timeCreated: 1456620041
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,110 @@
using System;
using UnityEngine;
using Random = UnityEngine.Random;
namespace Zenject.SpaceFighter
{
public class EnemyStateFollow : IEnemyState
{
readonly EnemyRotationHandler _rotationHandler;
readonly EnemyCommonSettings _commonSettings;
readonly Settings _settings;
readonly EnemyTunables _tunables;
readonly EnemyStateManager _stateManager;
readonly EnemyView _view;
readonly PlayerFacade _player;
bool _strafeRight;
float _lastStrafeChangeTime;
public EnemyStateFollow(
PlayerFacade player,
EnemyView view,
EnemyStateManager stateManager,
EnemyTunables tunables,
Settings settings,
EnemyCommonSettings commonSettings,
EnemyRotationHandler rotationHandler)
{
_rotationHandler = rotationHandler;
_commonSettings = commonSettings;
_settings = settings;
_tunables = tunables;
_stateManager = stateManager;
_view = view;
_player = player;
}
public void EnterState()
{
_strafeRight = Random.Range(0, 1) == 0;
_lastStrafeChangeTime = Time.realtimeSinceStartup;
}
public void ExitState()
{
}
public void Update()
{
if (_player.IsDead)
{
_stateManager.ChangeState(EnemyStates.Idle);
return;
}
var distanceToPlayer = (_player.Position - _view.Position).magnitude;
// Always look towards the player
_rotationHandler.DesiredLookDir = (_player.Position - _view.Position).normalized;
// Strafe back and forth over the given interval
// This helps avoiding being too easy a target
if (Time.realtimeSinceStartup - _lastStrafeChangeTime > _settings.StrafeChangeInterval)
{
_lastStrafeChangeTime = Time.realtimeSinceStartup;
_strafeRight = !_strafeRight;
}
if (distanceToPlayer < _commonSettings.AttackDistance)
{
_stateManager.ChangeState(EnemyStates.Attack);
}
}
public void FixedUpdate()
{
MoveTowardsPlayer();
Strafe();
}
void Strafe()
{
// Strafe to avoid getting hit too easily
if (_strafeRight)
{
_view.AddForce(_view.RightDir * _settings.StrafeMultiplier * _tunables.Speed);
}
else
{
_view.AddForce(-_view.RightDir * _settings.StrafeMultiplier * _tunables.Speed);
}
}
void MoveTowardsPlayer()
{
var playerDir = (_player.Position - _view.Position).normalized;
_view.AddForce(playerDir * _tunables.Speed);
}
[Serializable]
public class Settings
{
public float StrafeMultiplier;
public float StrafeChangeInterval;
public float TeleportNewDistance;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: f6b8435c02df92747b53470f32656b93
timeCreated: 1456620041
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,58 @@
using System;
using UnityEngine;
using Random = UnityEngine.Random;
namespace Zenject.SpaceFighter
{
public class EnemyStateIdle : IEnemyState
{
readonly EnemyRotationHandler _rotationHandler;
readonly Settings _settings;
readonly EnemyView _view;
Vector3 _startPos;
float _theta;
Vector3 _startLookDir;
public EnemyStateIdle(
EnemyView view, Settings settings,
EnemyRotationHandler rotationHandler)
{
_rotationHandler = rotationHandler;
_settings = settings;
_view = view;
}
public void EnterState()
{
_startPos = _view.Position;
_theta = Random.Range(0, 2.0f * Mathf.PI);
_startLookDir = _view.LookDir;
}
public void ExitState()
{
}
// Just add a bit of subtle movement
public void Update()
{
_theta += Time.deltaTime * _settings.Frequency;
_view.Position = _startPos + _view.RightDir * _settings.Amplitude * Mathf.Sin(_theta);
_rotationHandler.DesiredLookDir = _startLookDir;
}
public void FixedUpdate()
{
}
[Serializable]
public class Settings
{
public float Amplitude;
public float Frequency;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 7a99ffa8c241c8a4e97392c7903712f0
timeCreated: 1456618194
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: