Character-Bow-Template

Hi guys,
I’d like to share with you some ideas to improve my tecnical skill with your feedback. I like this game engine and it is difficult to find a good demos like FPS-Demo shared by @RiccardoBlb . So my take-away is share yours code, share yours ideas! It is the only way to grow up and have fun.

I have created a demo with third person shooter with mechanic for bow and arrow. It contains:

  • Physics (with bullet engine)
  • Animations (with gltf2 animations)
  • Third Person Camera with collision detection
  • Bow with two types of ammo and impact effect/sound
  • Dynamic update of camera FOV when aiming
  • Keyboard/Mouse and Joystick support
  • A reusable code and a game programming patterns very easy to understand

You can find the complete project, lib and file (sounds, blend file, …) here:
https://github.com/capdevon/Character-Bow-Template

10 Likes

We share them at the software store. There’s a link in the header.

Yes I know the JMonkeyStore, it is a great instrument but unfortunately at the moment, there are few projects. I like your initiative @jayfella with simple-fps-template and the replay of @RiccardoBlb with FPS-Demo-Template, I want to follow your example. I’d like to know what you think about my template and how to improve or optimize it. My invite is open to all community: share your ideas with me. Leave your feedback. Thanks

1 Like

You can add your project to the store then :+1:
I would clean the repo by removing the dll and other binary files that are extracted at runtime, if possible it would be probably useful to have it configured with gradle or maven to make it easily importable or buildable outside of netbeans.
From what i can see, the repo doesn’t have a license, meaning the code can’t be used by anyone, you should probably add one.
If you plan to publish it on the store you should probably add more images (or maybe a video?) to show it in action.

1 Like

what license you suggest to use ?

1 Like

BSD 3 clause license, like the engine. Or MIT that is very similar.
Or Unlicense if you don’t care about credits and want to release it as public domain.

Those are my choices usually

1 Like

done, I used Unlicense

1 Like

I configured project with gradle.
Here the new link:

https://github.com/capdevon/Archer-Game-Template

Are there any functions that can be improved? what features would you like to add?
Write here your ideas and your code to expand the project and make it useful and interesting for all developers.

2 Likes

Archer-Game-Template is very cool! Thank you for sharing your work.

Here are my questions/ideas:

  1. I’m struggling with the keyboard controls. Are they documented anywhere?
  2. The bow and the arrows should be made visible.
  3. I’d like it if releasing the R key (at full draw) fired an arrow. Having to press a separate button feels awkward.
  4. It seems like arrows in flight aren’t affected by gravity. If so, they should be.
  5. It would be more exciting if there were something to shoot at besides sliding crates. Perhaps something destructible and/or something that moves independently and/or something that fights back.
  6. It would be nice if they game kept score and kept track of elapsed time.
  7. For physics, you should use Minie in place of jme3-bullet. At this point, the changes required to switch would be about 7 lines of code.
1 Like

Thank you so much @sgold for your feedback. Let’s analyze the list by points:

  1. I have updated the README.md file with the explanation of the keyboard controls.
  2. I already have a test case, I’m simplifying the code to make it simple and reusable with other weapons.
  3. I reconfigured the keyboard controls to aim (E key) and change arrow type (R key) by placing them closer together.
    I hope this new configuration is less stressful on the wrist and easier to use.
    The aim and fire buttons are separate like in most third-person video games. They are very cool when used with the L1 and R1 buttons on the joystick. This is just my opinion, the controls are still easy to change (Archer-Game-Template/GInputAppState.java at main · capdevon/Archer-Game-Template · GitHub).
  4. You are absolutely right, the arrow is not affected by gravity. For simplicity I opted for an instant shot using a physics raycast to identify the affected surface (see line Archer-Game-Template/Physics.java at 1beec43d0cd3537e45b4c9619c77c52b46531ace · capdevon/Archer-Game-Template · GitHub). It is no coincidence that the origin of the ray is above the character’s head to avoid it being on the trajectory, and is oriented with the direction of the camera. I’d like to implement your idea, but haven’t been able to make a shoulder camera prototype yet.
    I could use this example (jmonkeyengine/BombControl.java at master · jMonkeyEngine/jmonkeyengine · GitHub)
    to handle bullet physics and object impacts, still don’t have an artistic idea of ​​how to harmoniously integrate this mechanic with third-person camera and weapons.
    Any suggestions with images or example code would help me a lot.
  5. I agree with you, it would be cooler, I already have protoypes with zombies too. If you have specific references in mind send me the links.
  6. like above
  7. I’d like to use your new library with all the new features, it’s been on my list for a while now. If you send me some PR on github on how to modify the build.gradle file and the 7 lines of code I am happy to accept them.

I’d like to turn this template into a free sample project that contains most of the basic mechanics used in third-person video games, and at the same time shows how to use all the jMonkeyEngine tools like Unity did with FPS Microgame - Unity Learn

1 Like

Thanks for you thoughtful responses to my feedback.

#4: A single raycast is fine if your weapon is a laser, but it seems strange given that the avatar’s body motions are clearly those of an archer. Once the arrow is released, its path should not depend on camera position. For the aim indicator, I suggest using a series of raycasts to estimate the impact point. I’ll send you some code soon.
#7: I’ll submit a PR for switching to MInie ASAP!

One other concern: I hear that you want this to become a template for creating FPS games. I’m not a serious gamer, but to me, “first-person” implies that the player’s avatar is not visible, except possibly for hands/weapons. In this game, the controls are FPS-like, but the camera looks over the avatar’s shoulder from a distance. So is this actually an FPS?

1 Like

Of course it is not an FPS. What I would like to do is a Unity-like tutorial but in third person. At the moment I’m trying to improve the movement and aiming mechanics. I already have some cool prototypes for melee attacks too. I will be a little busy at work in the next few days, but I will publish all the material I have collected over time creating some interesting discussions. I await your feedback!

2 Likes

I was confused about your intent. Sorry!

Here is the method (from my closed-source game) that calculates the trajectory of a missile (out to 3 seconds) and predicts its point of impact:

    /**
     * Predict the point of impact for a missile.
     *
     * @return a new location vector, or null for no prediction
     */
    public Vector3f predictPoi() {
        BulletAppState bas = BatsV2.findAppState(BulletAppState.class);
        PhysicsSpace physicsSpace = bas.getPhysicsSpace();
        Vector3f gravity = physicsSpace.getGravity(null);

        Vector3f location = initialLocation.clone();
        Vector3f previousLocation = new Vector3f();
        Vector3f velocity = velocity(null);
        float timeStep = 0.02f;
        for (int stepIndex = 0; stepIndex < 150; ++stepIndex) {
            previousLocation.set(location);
            MyVector3f.accumulateScaled(location, velocity, timeStep);
            MyVector3f.accumulateScaled(velocity, gravity, timeStep);
            List<PhysicsRayTestResult> rayTest
                    = physicsSpace.rayTestRaw(previousLocation, location);
            /*
             * Find the closest contact.
             */
            float closestFraction = 9f;
            for (PhysicsRayTestResult hit : rayTest) {
                float hitFraction = hit.getHitFraction();
                if (hitFraction < closestFraction) {
                    closestFraction = hitFraction;
                }
            }

            if (closestFraction <= 1f) {
                MyVector3f.lerp(closestFraction, previousLocation, location,
                        location);
                return location;
            }
        }

        return null;
    }
1 Like

thank you, I’m getting the gist of this, cool, but with a working example everything would become clear immediately. Would you like to add this functionality to the project? It could become a nice study project for community programmers.

2 Likes

I’ll throw together a sample application and add it to the MinieExamples project.

1 Like

Here you go:

Minie/HelloPoi.java at master · stephengold/Minie · GitHub

1 Like

I downloaded and compiled the MinieExamples project. The idea is very interesting, I calmly study the solution. If in doubt I will let you know. Thanks

1 Like

I’m improving the archer demo by adding physical arrows. At the moment they are just spheres, but I will replace them with the model of an arrow in the final version.

To test the collisions I built a scenario that includes:

  • A city scene with static RigidBodyControl and mass 0
  • Simple cubes with mass greater than 0
  • Sinbad to which I added the ‘DynamicAnimControl’ controller to identify the part of the body hit by the arrow.

The goal I would like to achieve is to attach the arrow to the hit surface or body parts of animated characters.

The algorithm I wrote seems to work correctly for cubes, while with Sinbad the arrows (balls) sometimes correctly follow the position of the moving bone, other times they don’t.
(see method ArrowControl.stick(PhysicsCollisionObject other, Vector3f hitPoint) )

Could you give me some suggestions on how to improve the solution?

Here is an example video.

Here is the code.

public class SinbadRagdollPrefab extends PrefabComponent {

	public SinbadRagdollPrefab(Application app) {
		super(app);
	}

	@Override
	public Spatial getAssetModel() {
		return app.getAssetManager().loadModel("Models/Sinbad/Sinbad.mesh.xml");
	}

	@Override
	public Spatial instantiate(Vector3f position, Quaternion rotation, Node parent) {
		Spatial model = getAssetModel();
		model.setLocalTranslation(position);
		model.setLocalRotation(rotation);
		parent.attachChild(model);
		
		AnimComposer composer = model.getControl(AnimComposer.class);
		composer.setCurrentAction("Dance");
		
		DynamicAnimControl ragdoll = new DynamicAnimControl();
		setupSinbad(ragdoll);
		model.addControl(ragdoll);
		PhysicsSpace.getPhysicsSpace().add(ragdoll);
		
		return model;
	}
	
	private void setupSinbad(DynamicAnimControl ragdoll) {
		ragdoll.link("Waist", 1f, new RangeOfMotion(1f, -0.4f, 0.8f, -0.8f, 0.4f, -0.4f));
		ragdoll.link("Chest", 1f, new RangeOfMotion(0.4f, 0f, 0.4f));
		ragdoll.link("Neck", 1f, new RangeOfMotion(0.5f, 1f, 0.7f));

		ragdoll.link("Clavicle.R", 1f, new RangeOfMotion(0.3f, -0.6f, 0f, 0f, 0.4f, -0.4f));
		ragdoll.link("Humerus.R", 1f, new RangeOfMotion(1.6f, -0.8f, 1f, -1f, 1.6f, -1f));
		ragdoll.link("Ulna.R", 1f, new RangeOfMotion(0f, 0f, 1f, -1f, 0f, -2f));
		ragdoll.link("Hand.R", 1f, new RangeOfMotion(0.8f, 0f, 0.2f));

		ragdoll.link("Clavicle.L", 1f, new RangeOfMotion(0.6f, -0.3f, 0f, 0f, 0.4f, -0.4f));
		ragdoll.link("Humerus.L", 1f, new RangeOfMotion(0.8f, -1.6f, 1f, -1f, 1f, -1.6f));
		ragdoll.link("Ulna.L", 1f, new RangeOfMotion(0f, 0f, 1f, -1f, 2f, 0f));
		ragdoll.link("Hand.L", 1f, new RangeOfMotion(0.8f, 0f, 0.2f));

		ragdoll.link("Thigh.R", 1f, new RangeOfMotion(0.4f, -1f, 0.4f, -0.4f, 1f, -0.5f));
		ragdoll.link("Calf.R", 1f, new RangeOfMotion(2f, 0f, 0f, 0f, 0f, 0f));
		ragdoll.link("Foot.R", 1f, new RangeOfMotion(0.3f, 0.5f, 0f));

		ragdoll.link("Thigh.L", 1f, new RangeOfMotion(0.4f, -1f, 0.4f, -0.4f, 0.5f, -1f));
		ragdoll.link("Calf.L", 1f, new RangeOfMotion(2f, 0f, 0f, 0f, 0f, 0f));
		ragdoll.link("Foot.L", 1f, new RangeOfMotion(0.3f, 0.5f, 0f));
	}

}
public class ArrowPrefab extends RangedBullet {
	
	public ArrowPrefab(Application app, String name) {
		super(app);
		mass = 6f;
		this.name = name;
	}

	@Override
	public Spatial getAssetModel() {
		// TODO Auto-generated method stub
		Mesh mesh = new Sphere(16, 16, 0.05f);
		Geometry geo = new Geometry("Arrow", mesh);
		Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
		mat.setColor("Color", ColorRGBA.Green.clone());
		geo.setMaterial(mat);
		return geo;
	}
	
	@Override
	public Spatial instantiate(Vector3f position, Quaternion rotation, Node parent) {
    	Spatial model = getAssetModel();
    	model.setName(name + "-" + nextSeqId());
    	model.setLocalTranslation(position);
    	model.setLocalRotation(rotation);
    	parent.attachChild(model);
    	
    	// Add Physics.
        RigidBodyControl rgb = new RigidBodyControl(mass);
        model.addControl(rgb);
        PhysicsSpace.getPhysicsSpace().add(rgb);
        rgb.setCollisionGroup(PhysicsCollisionObject.COLLISION_GROUP_02);
        rgb.setCollideWithGroups(PhysicsCollisionObject.COLLISION_GROUP_01);
        rgb.setCcdMotionThreshold(0.001f);
        
        ArrowControl arrow = new ArrowControl();
        model.addControl(arrow);
    	
    	return model;
    }

}
public class ArrowControl extends AdapterControl implements PhysicsCollisionListener {
	
	private static final Logger logger = Logger.getLogger(ArrowControl.class.getName());
	
	private RigidBodyControl rigidBody;
	private PhysicsSpace m_PhysicsSpace;
	private boolean hasCollided;
	private float maxFlyingTime = 10f;
	private float timer = 0;

	@Override
	public void setSpatial(Spatial sp) {
		super.setSpatial(sp);
		if (spatial != null) {
			rigidBody = getComponent(RigidBodyControl.class);
			m_PhysicsSpace = rigidBody.getPhysicsSpace();
			m_PhysicsSpace.addCollisionListener(this);
		}
	}
	
    @Override
    protected void controlUpdate(float tpf) {
        // TODO Auto-generated method stub
    	timer += tpf;
    	
    	if (!hasCollided) {
    		// this is to cleanup old bullets that hasn't collided yet (lived more than maxTime)
    		if (timer > maxFlyingTime) {
    			destroy();
    		}
    	}
    }
    
	@Override
	public void collision(PhysicsCollisionEvent event) {
		if (hasCollided) {
			return;
		}
		
		if ( event.getObjectA() == rigidBody || event.getObjectB() == rigidBody ) {
			
			hasCollided = true;

			// Stop the rigidBody in position
			rigidBody.setEnabled(false);

			PhysicsCollisionObject other;
			Vector3f hitPoint;
			
			if (event.getObjectA() == rigidBody) {
                other = event.getObjectB();
                hitPoint = event.getPositionWorldOnB();
            } else {
                other = event.getObjectA();
                hitPoint = event.getPositionWorldOnA();
            }
			
			logger.log(Level.INFO, "Collided with: {0}", other.getUserObject().toString());
			stick(other, hitPoint);
			
			spatial.addControl(new TimerControl(15f) {
				@Override
				public void onTrigger() {
					destroy();
				}
			});
		}
	}
	
	/**
	 * 
	 * @param other
	 * @param hitPoint
	 */
	private void stick(PhysicsCollisionObject other, Vector3f hitPoint) {
		
		if (other.getUserObject() instanceof Node) {
			Node gameObject = (Node) other.getUserObject();
			gameObject.worldToLocal(hitPoint, hitPoint);
			gameObject.attachChild(spatial);
        	spatial.setLocalTranslation(hitPoint);
        	
		} else if (other.getUserObject() instanceof BoneLink) {
			BoneLink bone = (BoneLink) other.getUserObject();
        	Spatial animRoot = bone.getControl().getSpatial();
        	Node attachNode = animRoot.getControl(SkinningControl.class).getAttachmentsNode(bone.boneName());
        	System.out.println(bone.boneName() + " " + animRoot + "; " + attachNode);
        	
        	attachNode.worldToLocal(hitPoint, hitPoint);
        	attachNode.attachChild(spatial);
        	spatial.setLocalTranslation(hitPoint);
        	
		} else {
			logger.log(Level.WARNING, "Unable to attach the arrow to the hit object: " + other.getUserObject());
		}
	}
	
	//  Destroy everything
	private void destroy() {
		m_PhysicsSpace.removeCollisionListener(this);
		spatial.removeFromParent();
		logger.log(Level.INFO, "Object Destroyed: {0}", spatial);
	}

}

Let me know if you need more information on the code I wrote.
I’ll post the demo on github with the final code.

@sgold @oxplay2 @Ali_RS

3 Likes

@capdevon I thought it can be done easily through removal of the arrows from physical space onCollision & donot remove the arrows Spatials from the rootNode after collision , am I right ?

EDIT :

This is a great part , I tried sticking objs before but on non-movable surfaces , so this was the trick for movable or animated objects good part :smiley:

But what does worldToLocal() intended to do ?

1 Like

To connect 2 physics bodies (such as an arrow and a target), the natural solution is to add a PhysicsJoint such as New6Dof. For bodies that penetrate other bodies, you can disable collisions using setCollisionBetweenLinkedBodies​(false).

  • Sticking based on spatials doesn’t seem like a good plan. Spatials are for rendering, not for representing game objects.
  • Putting rigid bodies in different collision groups doesn’t seem like a good plan, because it prevents arrows from colliding with other arrows.
1 Like