I’ve already done some testing and played around Unity. At this point, I’ve figured out basic implementation for aiming and throwing, but I decided to restart fresh, as the existing project became very messy and un-organized due to the testing and a lot of unplanned coding. From now on, I’ll try to do my testing on a separated, throw-away project so the debug codes and test objects won’t mess up the actual project.
Aside from that, I’ve also purchased the Polygon Samurai pack from Synty Studio on Unity asset store. I think it should fit the game quite well.
So first of all I setup a basic VR project like this. Next I imported the asset and updated the materials to URP. Different from my first try, this time I’m going to setup the environment first, including ground, buildings, lighting, etc. Apparently building the environment takes a lot of time, I figured that it’s not worth it to spend too much time on that. For now I might just use the demo scene came from the pack, and focus on implementing the mechanics and gameplay.
To implement the aiming laser, I made a prefab named AimLaser
, with a LineRenderer
component and a AimLaser
script. Each hand controller will have two of the AimLaser
prefab, with the rotation tweaked to face opposite direction.
Now, I want only the laser pointing towards the (roughly) same direction the player is facing to be the active one, and disable the one pointing backwards. I added a script named ThrowHelper
and add it as a component to each hand controller.
When the grip button is triggered, it locks the laser position and direction, then spawn a projectile in hand, and make it follow the hand movement:
// lock the aim laser
aimLocked = true;
forehandLaser.setLocked(true);
backhandLaser.setLocked(true);
// Spawn projectile in hand
projectileInHand = Instantiate(projectilePrefab, gameObject.transform.position, gameObject.transform.rotation);
projectileInHand.transform.parent = gameObject.transform;
If the grip button is released, it unlocks the lasers and destroy the projectile if it’s still in the hand (not thrown):
// unlock aim laser
aimLocked = false;
forehandLaser.setLocked(false);
backhandLaser.setLocked(false);
// destroy porjectile if not shoot yet
if (projectileInHand != null)
{
Destroy(projectileInHand);
projectileInHand = null;
}
While the aim is not locked, it determines which laser is pointing forward in each update cycle and enable/disable relevant laser:
forehandActive = Vector3.Angle(forehandLaser.transform.forward, camTransform.forward) <= 90
forehandLaser.setIsForward(forehandActive);
backhandLaser.setIsForward(!forehandActive);
Whereas if the aim is locked, the script checks the hand travel speed and travel direction. If it’s swinging towards the aim and is above certain threshold, throw the projectile
if (Vector3.Angle(handTravel, activeLaser.transform.forward) <= 90 // hand is swinging forward
&& handTravel.sqrMagnitude / (Time.deltaTime * Time.deltaTime) > sqrSpeedThreshold) // and speed is above threshold
{
Throw(activeLaser);
}
To throw the projectile, we first unlock the aim, then calculate the direction to throw and pass it into the projectile script. Then remove reference from the projectileInHand.
aimLocked = false;
forehandLaser.setLocked(false);
backhandLaser.setLocked(false);
// shoot shuriken
if (projectileInHand == null)
return;
Projectile projectile = projectileInHand.GetComponent<Projectile>();
if (projectile == null)
return;
Vector3 firstWaypoint = activeLaser.lineRenderer.GetPosition(1);
Vector3 travelDirection = (activeLaser.lineRenderer.GetPosition(1) - activeLaser.lineRenderer.GetPosition(0)).normalized;
projectile.Throw(firstWaypoint, travelDirection);
projectileInHand = null;
In the projectile script, the Throw function sets the shooted
variable to true, unlink the transform from the parent, so it’s not following the hand anymore, and also set’s the rotation towards the traveling direction. It’ll also destroy the projectile 5 seconds after shooting.
shooted = true;
transform.parent = null; // unlink from parent transform
transform.LookAt(transform.position + direction);
this.firstWaypoint = firstWaypoint;
this.direction = direction;
Destroy(gameObject, destroyAfter);
In every update cycle, we check if the projectile has reached the first waypoint or not. If it did, it’ll travel towards the direction set; if not, it’ll travel towards the first waypoint.
if (hit)
return;
if (!shooted)
return;
if (reachedFirstPoint)
{
transform.Translate(direction * speed * Time.deltaTime, Space.World);
} else
{
Vector3 firstPointDirection = firstWaypoint - transform.position;
float travelDistance = speed * Time.deltaTime;
if (travelDistance >= firstPointDirection.magnitude)
{
reachedFirstPoint = true;
}
transform.Translate(firstPointDirection.normalized * travelDistance, Space.World);
}
// spin
transform.Rotate(Vector3.up, rotationPerSec * Time.deltaTime);
If the projectile hit something, check if it’s hitting another projectile, if so, enable gravity so the two projectile will fall to the ground. If not, then remove rigid body as we don’t need to check collision and physics anymore. It’ll also apply a force on the collided object and spawn an impact effect.
if (hit || other.CompareTag("Player"))
return;
hit = true;
transform.parent = other.transform;
if (rb != null)
{
Destroy(gameObject.GetComponent<Collider>());
if (other.CompareTag(projectileTag)) // if hit other projectile
{
rb.useGravity = true; // enable gravity, so two projectile will fall after collide to each other
}
else
{
Destroy(gameObject.GetComponent<Rigidbody>()); // remove rigid body
}
rb = null;
}
if (!other.gameObject.isStatic)
{
Rigidbody otherRb = other.GetComponent<Rigidbody>();
if (otherRb != null)
otherRb.AddForceAtPosition(direction * impactForce, transform.position);
}
if (impactEffect != null)
{
GameObject effect = Instantiate(impactEffect, transform);
effect.transform.LookAt(firstWaypoint);
if (!reachedFirstPoint)
effect.transform.Rotate(0, 180, 0);
Destroy(effect, 2);
}
Summary of this devlog: I’ve implemented the aiming, throwing, hitting mechanics.