Shooter Gameplay Mechanics - A little math

Hi guys,
I wanted to share with you a solution widely used in shooting mechanics. In a friendly way I will tell you the conclusions I have reached, just to share some reflections.
Recently I was trying to convert the following instructions of a game demo created with Unity, from C# to java.

    public Vector3 GetShotDirectionWithinSpread(Transform shootTransform)
    {
        float spreadAngleRatio = bulletSpreadAngle / 180f;
        Vector3 spreadWorldDirection = Vector3.Slerp(shootTransform.forward, UnityEngine.Random.insideUnitSphere, spreadAngleRatio);
        return spreadWorldDirection;
    }

The code is used to generate random variations in the trajectory of the bullets. Used a lot to model the damage caused by shotgun ammunition.
I noticed that the Vector3f class does not contain two functions that I often encounter in Unity tutorials:

After some research I found that the math behind it is quite complex, but the Vector3 class of the libgdx library already contained a good implementation of these functions, so I adapted them with the objects of our JMonkeyEngine.

from libgdx:


	/** Spherically interpolates between this vector and the target vector by alpha which is in the range [0,1]. The result is
	 * stored in this vector.
	 *
	 * @param target The target vector
	 * @param alpha The interpolation coefficient
	 * @return This vector for chaining. */
	public Vector3 slerp (final Vector3 target, float alpha) {
		final float dot = dot(target);
		// If the inputs are too close for comfort, simply linearly interpolate.
		if (dot > 0.9995 || dot < -0.9995) return lerp(target, alpha);

		// theta0 = angle between input vectors
		final float theta0 = (float)Math.acos(dot);
		// theta = angle between this vector and result
		final float theta = theta0 * alpha;

		final float st = (float)Math.sin(theta);
		final float tx = target.x - x * dot;
		final float ty = target.y - y * dot;
		final float tz = target.z - z * dot;
		final float l2 = tx * tx + ty * ty + tz * tz;
		final float dl = st * ((l2 < 0.0001f) ? 1f : 1f / (float)Math.sqrt(l2));

		return scl((float)Math.cos(theta)).add(tx * dl, ty * dl, tz * dl).nor();
	}

	/** Sets the components from the given spherical coordinate
	 * @param azimuthalAngle The angle between x-axis in radians [0, 2pi]
	 * @param polarAngle The angle between z-axis in radians [0, pi]
	 * @return This vector for chaining */
	public Vector3 setFromSpherical (float azimuthalAngle, float polarAngle) {
		float cosPolar = MathUtils.cos(polarAngle);
		float sinPolar = MathUtils.sin(polarAngle);

		float cosAzim = MathUtils.cos(azimuthalAngle);
		float sinAzim = MathUtils.sin(azimuthalAngle);

		return this.set(cosAzim * sinPolar, sinAzim * sinPolar, cosPolar);
	}

	@Override
	public Vector3 setToRandomDirection () {
		float u = MathUtils.random();
		float v = MathUtils.random();

		float theta = MathUtils.PI2 * u; // azimuthal angle
		float phi = (float)Math.acos(2f * v - 1f); // polar angle

		return this.setFromSpherical(theta, phi);
	}

to jme3:

public class FVector {

    /**
     * Spherically interpolates between start vector and the end vector by
     * alpha which is in the range [0,1].
     *
     * @param start The start vector
     * @param end The end vector
     * @param alpha The interpolation coefficient
     * @return The result vector
     */
    public static Vector3f slerp(final Vector3f start, final Vector3f end, float alpha) {
        final float dot = start.dot(end);
        // If the inputs are too close for comfort, simply linearly interpolate.
        if (dot > 0.9995 || dot < -0.9995) {
            return new Vector3f().interpolateLocal(start, end, alpha);
        }
        // theta0 = angle between input vectors
        final float theta0 = (float) Math.acos(dot);
        // theta = angle between this vector and result
        final float theta = theta0 * alpha;
        final float st = (float) Math.sin(theta);
        final float tx = end.x - start.x * dot;
        final float ty = end.y - start.y * dot;
        final float tz = end.z - start.z * dot;
        final float l2 = tx * tx + ty * ty + tz * tz;
        final float dl = st * ((l2 < 0.0001f) ? 1f : 1f / (float) Math.sqrt(l2));
        return start.scaleAdd((float) Math.cos(theta), new Vector3f(tx * dl, ty * dl, tz * dl)).normalizeLocal();
    }
        
    /**
     * Sets the components from the given spherical coordinate
     *
     * @param azimuthalAngle The angle between x-axis in radians [0, 2pi]
     * @param polarAngle The angle between z-axis in radians [0, pi]
     * @return This vector for chaining
     */
    public static Vector3f setFromSpherical(float azimuthalAngle, float polarAngle) {
        float cosPolar = FastMath.cos(polarAngle);
        float sinPolar = FastMath.sin(polarAngle);

        float cosAzim = FastMath.cos(azimuthalAngle);
        float sinAzim = FastMath.sin(azimuthalAngle);

        return new Vector3f(cosAzim * sinPolar, sinAzim * sinPolar, cosPolar);
    }

    public static Vector3f insideUnitSphere() {
        float u = FastMath.nextRandomFloat();
        float v = FastMath.nextRandomFloat();

        float theta = FastMath.TWO_PI * u; // azimuthal angle
        float phi = (float) Math.acos(2f * v - 1f); // polar angle

        return setFromSpherical(theta, phi);
    }
}

Here is the final result. I hope my story will be useful to you. Let me know if I have made any mistakes or if there are simpler functions that I may not have found.


public class Main {

   ...

   private Shotgun weapon;
   private Node bulletNodeGroup;
   private Geometry bulletGeo;

    @Override
    public void onAction(String name, boolean isPressed, float tpf) {
        if (name.equals("shooting") && isPressed) {
            shooting();
        }
    }
    
    private void shooting() {
        Vector3f origin = cam.getLocation();

        for (int i = 0; i < weapon.bulletsPerShot; i++) {
            Vector3f shotDirection = weapon.getShotDirectionWithinSpread(cam.getDirection());
            Spatial g = instantiate(bulletGeo, origin, Quaternion.IDENTITY, bulletNodeGroup);
            g.addControl(new BulletControl(bulletAppState, origin, shotDirection));
        }
    }
    
    private void initBulletPrefab() {
        Sphere sphere = new Sphere(32, 32, .05f);
        bulletGeo = new Geometry("Bullet.GeoMesh", sphere);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Red);
        bulletGeo.setMaterial(mat);
    }
    
    private Spatial instantiate(Spatial model, Vector3f position, Quaternion rotation, Node parent) {
        Spatial sp = model.clone();
        sp.setLocalTranslation(position);
        sp.setLocalRotation(rotation);
        parent.attachChild(sp);
        return sp;
    }

   ...
}
public class Shotgun extends IWeapon {
    ...

    // Angle for the cone in which the bullets will be shot randomly (0 means no spread at all)
    float bulletSpreadAngle = 5f;
    // Amount of bullets per shot
    int bulletsPerShot = 10;

    public Vector3f getShotDirectionWithinSpread(Vector3f shotDirection) {
        float spreadAngleRatio = bulletSpreadAngle / 180f;
        Vector3f spreadWorldDirection = FVector.slerp(shotDirection, FVector.insideUnitSphere(), spreadAngleRatio);
        return spreadWorldDirection;
    }

   ...
}


4 Likes

Full code of FVector class here:

about “Vector3.slerp”:

not sure about difference. Maybe you could explain me.
maybe im wrong, but i think its just “interpolate” with “normalizeLocal” on vectors before doing it. like

vector1.normalizeLocal().interpolateLocal(vector2.normalizeLocal(), tValue)

about “Random.insideUnitSphere”:
you might be right, at least im not sure if it exist here, but you can easly check if Vector3f.isUnit() that means same, just check, not create new. So its very easy to add method that random point until is match “Vector3f.isUnit()” and multiply by sphere size.

also just curious, why only 30 fps? low hardware? if its just screenapp causing it, then nvm.

Just FYI, i would use physics for bullets with low CCThreshold, you could try it :slight_smile: just set initial Vector(direction) velocity like you want do and check collision of physics.

edit:

The code is used to generate random variations in the trajectory of the bullets

so you mean spread the bullet vectors?

why not just code like:

mainWeaponDirectionVector.clone().interpolateLocal(getRandomSphereUnitVector(), Math.random()*spreadValue); // spreadValue like 0.1f

where make “getRandomSphereUnitVector()” like i said.

1 Like

thanks for your reply @oxplay2. The original idea was to use a simple raycast to locate objects in the linear trajectory of bullets without using physics to verify collisions. I added the spheres to the demo just to show the starting trajectories.

But you gave me an idea, I could add physics as you suggested to more realistically model the arrows in the template I uploaded to github. We’ll talk about it again as soon as I’ve built a sample code :wink:

I ran a test with the formula you suggested and I noticed you were right, the final result is almost the same, however with your formula the initial dispersion of the bullets is slightly lower, which is not a problem, I also like this as a result (see figure below)

PS: I increased the number of fps :laughing:

before:

after:

The trajectories generated by the two functions with an angle of 5 degrees.

	...

	for (int i = 0; i < weapon.bulletsPerShot; i++) {
		Vector3f shotDirection = printTrajectories(cam.getDirection());
		...
	}
	
    private Vector3f printTrajectories(Vector3f shootDirection) {
        Vector3f insideUnitSphere = FVector.insideUnitSphere();
        System.out.println("FVector.insideUnitSphere: " + insideUnitSphere);

        float bulletSpreadAngle = weapon.bulletSpreadAngle; //5f
        float spreadAngleRatio = bulletSpreadAngle / 180f;
        System.out.println("spreadAngleRatio: " + spreadAngleRatio);
        
        //your formula
        Vector3f lerp = shootDirection.clone().interpolateLocal(insideUnitSphere, spreadAngleRatio); 
        //my formula
        Vector3f slerp = FVector.slerp(shootDirection, insideUnitSphere, spreadAngleRatio);

        System.out.println("FVector.slerp: " + slerp);
        System.out.println("interpolateLocal: " + lerp);
        
        return lerp;
    }
	
	...
	

FVector.insideUnitSphere: (-0.9420008, -0.19484894, -0.27325508)
spreadAngleRatio: 0.027777778
FVector.slerp: (-0.2835226, 0.3261077, 0.9018141)
interpolateLocal: (-0.2587263, 0.31941155, 0.8787624)
FVector.insideUnitSphere: (-0.3467085, -0.40460965, -0.84621763)
spreadAngleRatio: 0.027777778
FVector.slerp: (-0.30618355, 0.31719324, 0.89757466)
interpolateLocal: (-0.24219042, 0.31358483, 0.8628468)
FVector.insideUnitSphere: (-0.20483422, 0.8650173, 0.45802632)
spreadAngleRatio: 0.027777778
FVector.slerp: (-0.23988692, 0.35259157, 0.90450734)
interpolateLocal: (-0.23824947, 0.34885228, 0.8990758)
FVector.insideUnitSphere: (0.058445077, 0.88893837, -0.45428267)
spreadAngleRatio: 0.027777778
FVector.slerp: (-0.23764585, 0.3782056, 0.8946983)
interpolateLocal: (-0.23093614, 0.34951675, 0.8737339)
FVector.insideUnitSphere: (0.86786544, -0.43842697, 0.2336478)
spreadAngleRatio: 0.027777778
FVector.slerp: (-0.19887315, 0.31493402, 0.9280443)
interpolateLocal: (-0.20845225, 0.31264547, 0.89284307)
FVector.insideUnitSphere: (-0.85587937, 0.462829, 0.23078115)
spreadAngleRatio: 0.027777778
FVector.slerp: (-0.26257673, 0.3428672, 0.90194)
interpolateLocal: (-0.25633404, 0.33768037, 0.89276344)
FVector.insideUnitSphere: (-0.19499111, -0.7314168, -0.6534584)
spreadAngleRatio: 0.027777778
FVector.slerp: (-0.28228167, 0.2804121, 0.91743463)
interpolateLocal: (-0.23797604, 0.30450687, 0.8682012)
FVector.insideUnitSphere: (0.9477145, 0.28780738, -0.13785563)
spreadAngleRatio: 0.027777778
FVector.slerp: (-0.19230163, 0.353301, 0.9155319)
interpolateLocal: (-0.20623422, 0.33281866, 0.88252354)
FVector.insideUnitSphere: (-0.58415806, -0.79283315, -0.17370935)
spreadAngleRatio: 0.027777778
FVector.slerp: (-0.27396414, 0.29609925, 0.915024)
interpolateLocal: (-0.24878624, 0.30280086, 0.8815276)
FVector.insideUnitSphere: (0.48200634, -0.7010107, -0.5255986)
spreadAngleRatio: 0.027777778
FVector.slerp: (-0.20275669, 0.27968842, 0.9384371)
interpolateLocal: (-0.21917056, 0.3053515, 0.87175286)

Regarding the insideUnitSphere function I had found this article that shows in real time the random points generated within a sphere of unit radius centered in the origin using different approaches.
https://karthikkaranth.me/blog/generating-random-points-in-a-sphere/

then just change this “Math.random()*spreadValue” and use just spreadValue, or use same ,but increase spreadValue to 0.2

edit: oh i see you use some spreadAngleRatio.
then you just need increase spreadAngleRatio value.

Its not same algorithm since i seen a lot of code in your FVector, IMO my solution is just much much easier.

1 Like

you are absolutely right, your solution is much clearer and easier. The only function needed was insideUnitSphere, thanks for your help :wink:
I do some tests with bullet physics.

1 Like

Note that your insideUnitSphere() method (which is really kind of randomDirection(), no?) will tend to clump at the poles because of how it’s implemented. Randomly generated lat/lon style spherical coordinates will have this issue. (For example, no matter which of the random 360 degrees of polar angle you pick, any particularly high or particularly low azimuth will always be near other high/low azimuth values.)

There are better distributed approaches but they get more complicated the better distributed they are. I think the simplest is to just pick a random point in the -1…1 cube and then normalize it (accounting for 0,0,0 as a special value). I think those will clump a little towards the corners of the cube but not nearly as badly as lat/lon style spherical coordinates at the north/south pole.

The best is probably to pick a random point on the side of the -1…1 cube (trickier than it sounds) and then normalize that. Points will tend to clump slightly along the cube edges but it’s all much more random overall.