J29 Unleashed
Defend your forest from the invading turrets
Overview
Project Duration
4 Weeks
Game Engine
Unity
Inspiration/References
War Thunder
Airfix Dogfighter
Key Features
Flight Controller
Arcade controls
Adjustable and modular
Weapon Systems
Differing Weapons
Reactive Targeting System
Enemies
AA Turrets
Plane AI
Project Goals
Objective
Create a Flight Controller with Robust Combat Mechanics.
Simple AI to show the different systems
Focus Areas
Flight Dynamics
Weapon Integrations
Player Experience/Feedback
Background
This personal project has been a passion of mine to make ever since playing Airfix Dogfighter.
Over the past four weeks, I've focused on gameplay programming, specifically creating a player controller and weapon systems using Unity's Rigidbody for a semi-realistic feel.
This project highlights my strengths in gameplay programming and creative thinking.
Working alone has allowed me to explore new ideas independently.
However, I find that collaborating in a team environment enhances my creativity and allows for valuable feedback on my work.
I approach each project with an open mind, balancing simplicity with effective programming solutions.
Please take a moment to explore my page for J29 Unleashed!
Terrain Deformation
Using Unity's own terrain tool I created a script that deforms it based it's heightmap and position, I used this for the bombs, missile and when you explode.
IEnumerator CreateCraterCoroutine(Vector3 position, float radius, float depth)
{
Terrain terrain = Terrain.activeTerrain;
if (terrain == null) yield break;
TerrainData terrainData = terrain.terrainData;
Vector3 terrainPos = terrain.transform.position;
int heightmapWidth = terrainData.heightmapResolution;
int heightmapHeight = terrainData.heightmapResolution;
int xBase = (int)((position.x - terrainPos.x) / terrainData.size.x * heightmapWidth);
int zBase = (int)((position.z - terrainPos.z) / terrainData.size.z * heightmapHeight);
int craterRadiusInHeightmap = (int)(radius / terrainData.size.x * heightmapWidth);
int xStart = Mathf.Clamp(xBase - craterRadiusInHeightmap, 0, heightmapWidth);
int xEnd = Mathf.Clamp(xBase + craterRadiusInHeightmap, 0, heightmapWidth);
int zStart = Mathf.Clamp(zBase - craterRadiusInHeightmap, 0, heightmapHeight);
int zEnd = Mathf.Clamp(zBase + craterRadiusInHeightmap, 0, heightmapHeight);
int width = xEnd - xStart;
int height = zEnd - zStart;
float[,] heights = terrainData.GetHeights(xStart, zStart, width, height);
for (int x = 0; x < width; x++)
{
for (int z = 0; z < height; z++)
{
int xOffset = x + xStart - xBase + craterRadiusInHeightmap;
int zOffset = z + zStart - zBase + craterRadiusInHeightmap;
float distance = Vector2.Distance(new Vector2(xOffset, zOffset),
new Vector2(craterRadiusInHeightmap, craterRadiusInHeightmap));
if (distance < craterRadiusInHeightmap)
{
float proportionalDistance = distance / craterRadiusInHeightmap;
float heightAdjustment = (1 - proportionalDistance) * (depth / terrainData.size.y);
heights[z, x] -= heightAdjustment;
}
}
if (x % 10 == 0)
{
yield return null;
}
}
terrainData.SetHeights(xStart, zStart, heights);
}
Missile Controller
Using the Quadratic formula I calculate the optimal trajectory for intercepting a moving target based on their positions and velocities. MoveToTarget method utilizes this course to adjust the missile's velocity, ensuring it steers to intercept the target effectively creating a seeking missile, this interception calculation is also used for the ground turrets to know where to shoot.
Vector3 CalculateInterceptCourse(Vector3 _tPos, Vector3 _tSpeed, Vector3 _iPos, float _iSpeed)
{
Vector3 targetDir = _tPos - _iPos;
float iSpeedSquared = _iSpeed* _iSpeed;
float tSpeedSquared = _tSpeed.sqrMagnitude;
float forwardDot = Vector3.Dot(targetDir, _targetSpeed);
float targetDist = targetDir.sqrMagnitude;
float d = (forwardDot * forwardDot) - targetDist * (tSpeedSquared - iSpeedSquared);
if (d < 0.0f)
return targetDir.normalized;
float sqrt = Mathf.Sqrt(d);
float S1 = (-forwardDot - sqrt) / targetDist;
float S2 = (-forwardDot + sqrt) / targetDist;
float S = Mathf.Max(S1, S2);
return (targetDir * S + _tSpeed).normalized;
}
MoveToTarget()
{
outOfAngleTimer = 0f;
Vector3 targetPosition = target.transform.position;
Rigidbody targetRigidbody = target.GetComponent<Rigidbody>();
if (targetRigidbody != null)
{
Vector3 targetVelocity = targetRigidbody.velocity;
Vector3 interceptDirection = CalculateInterceptCourse(targetPosition,
targetVelocity, transform.position,
rb.velocity.magnitude);
rb.velocity = Vector3.RotateTowards(rb.velocity,
interceptDirection * currentSpeed + playerVelocity,
maxTurnSpeed * Time.deltaTime, 0.0f);
}
}
Flight Controller
Making a method for lift ensures the ability to be able to glide with the plane.
Aligning the gameobject's forward with it's rigibody's forward vector was crucial to remove any drifting.
ApplyLift()
{
float forwardSpeed = Vector3.Dot(rb.velocity, transform.forward);
float gravitationalForce = rb.mass * Physics.gravity.magnitude;
float liftForceMagnitude = liftMultiplier * airDensity * forwardSpeed * forwardSpeed * wingArea * liftCoefficient;
float speedFactor = Mathf.Clamp01((forwardSpeed - minSpeedForLift) / minSpeedForLift);
liftForceMagnitude = Mathf.Min(liftForceMagnitude, gravitationalForce);
liftForceMagnitude *= speedFactor;
Vector3 liftDirection = transform.up;
liftForce = liftDirection * liftForceMagnitude;
rb.AddForce(liftForce);
}
AlignVelocityWithForwardDirection()
{
float gravitationalForce = rb.mass * Physics.gravity.magnitude;
bool isLiftSufficient = liftForce.magnitude / 2.0f >= gravitationalForce;
float forwardSpeed = Vector3.Dot(rb.velocity, transform.forward);
float alignmentSpeed = isLiftSufficient ? maxAlignmentSpeed : minAlignmentSpeed;
Vector3 alignedVelocity = Vector3.Lerp(rb.velocity, transform.forward * forwardSpeed, alignmentSpeed);
rb.velocity = alignedVelocity;
}