Character-Bow-Template

thank you @Pavl_G, I always publish my code to give everyone the opportunity to learn something, even from my mistakes. It seems to work even without the worldToLocal statement.

Thanks @sgold for your patience. Could you please tell me how to use a PhysicsJoint / New6Dof in the stick method? It would help me understand better.

private void stick(PhysicsCollisionObject other, Vector3f hitPoint) {
		
		if (other.getUserObject() instanceof Node) {
			Node gameObject = (Node) other.getUserObject();
			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.attachChild(spatial);
        	spatial.setLocalTranslation(hitPoint);
        	
		} else {
			logger.log(Level.WARNING, "Unable to attach the arrow to the hit object: " + other.getUserObject());
		}
	}

I didn’t quite understand what you mean.

I have deliberately chosen to use a separate group for the arrows because in this scenario I don’t need collisions with other arrows.

1 Like

To create a PhysicsJoint, you’ll need access to both rigid bodies. I don’t know how to rewrite the stick() method because it apparently has access to only one of them.

Suppose rigid body of the arrow is mainBody, the rigid body of the target is closestBody, and the point of impact is at pivotLocation in physics-space coordinates. Then the essential code looks like this:

                /*
                 * Create a constraint, its pivot located at the point of impact
                 * and its axes aligned with those of the arrow...
                 */

                // transform the POI to the arrow's local coordinates
                Matrix3f closestMatrix = closestBody.getPhysicsRotationMatrix(null);
                closestMatrix.invertLocal();
                Vector3f closestOffset = closestBody.getPhysicsLocation(null);
                closestOffset.negateLocal();
                closestOffset.addLocal(pivotLocation);
                closestMatrix.mult(closestOffset, closestOffset);

                // locate the POI in the target's local coordinates
                Matrix3f mainMatrix = mainBody.getPhysicsRotationMatrix(null);
                closestMatrix.multLocal(mainMatrix);
                float penetrationFraction = 0.35f; // for 35% penetration
                Vector3f mainOffset = tipOffset.mult(1f - 2f * penetrationFraction);

                New6Dof constraint = new New6Dof(mainBody, closestBody,
                        mainOffset, closestOffset, Matrix3f.IDENTITY, closestMatrix, RotationOrder.XYZ);

                // disable all 3 rotational axes
                for (int axisIndex = 0; axisIndex < 3; ++axisIndex) {
                    RotationMotor rMotor = constraint.getRotationMotor(axisIndex);
                    rMotor.setSpringEnabled(true); // necessary, but not sure why!
                    int dofIndex = axisIndex + 3;
                    constraint.set(MotorParam.UpperLimit, dofIndex, 0f);
                    constraint.set(MotorParam.LowerLimit, dofIndex, 0f);
                }
                constraint.setCollisionBetweenLinkedBodies(false);
                physicsSpace.addJoint(constraint);

Game objects describe the state of the game. Spatials are objects JMonkeyEngine uses to organize a 3-D scene for rendering purposes.

In very simple games, one can get away with representing each game object as a Spatial and storing game state in userdata or scene-graph controls. But for games of moderate to high complexity, it’s prudent to create data structures outside the scene graph to keep track of game state.

To clarify what’s a game object, imagine writing a networked game where 3-D rendering and user interface are handled entirely by the client, while interactions between players and their environment are simulated entirely by the server. In this scenario, game objects are data maintained by the server. Since the server doesn’t do any 3-D rendering, there would be no scene graph on the server and hence no spatials.

For another example, consider a simple chess game. The game state for chess basically consists of (1) whose turn it is and (2) the locations of the 32 pieces (65 possible locations for each). [In practice a bit more is needed for castling, pawn promotion, and stalemate detection.] The point is, it would be silly to embed this information in a scene graph.

A third way to think about game objects is in terms of save/restore. Game objects represent information written to stable storage at each save point. Sky textures, no. Inventory, yes.

To speed your project along, here are some tricks I developed for simulating arrow dynamics:

  1. Before each physics tick, apply torque to turn the long axis of the arrow toward its direction of travel. This simulates an effect of fletching and helps the motion look realistic. It also increases the likelihood that the point of impact will be the arrow’s head (not its shaft or fletchings or nock). So software can focus on the motion of the head, paying less attention to the rest.
  2. Before each physics tick, project the motion of the arrowhead during the upcoming timestep. (Use a sweep test with a small sphere representing the arrowhead.) This allows software to anticipate penetrations before any physics collision occurs. That’s when it’s easiest to simulate penetration.
  3. If an arrowhead is predicted to collide with a penetrable object, reduce the arrow’s speed to keep it from colliding during the current timestep. An opportunity to transfer some of its momentum to the target.
  4. Bullet’s continuous collision detection (CCD) doesn’t work well for long, skinny objects. If an arrowhead is predicted to collide with an impenetrable object, reflect the arrow’s velocity against the object’s surface and diminish the speed by 50% or so. This gives a convincing simulation of a ricochet without relying on Bullet’s CCD, which would need a large swept sphere.
4 Likes

I’d like to learn, but managing the physical components isn’t easy. An example code would help me and everyone who reads the topic to understand something. I have tried to include your suggestions in ArrowControl, but I don’t think I really understand what to do. Here is my latest attempt:

public class ArrowControl extends AbstractControl implements PhysicsCollisionListener, PhysicsTickListener {
	
	private static final Logger logger = Logger.getLogger(ArrowControl.class.getName());
	
	// the rigid body of the arrow.
	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 = spatial.getControl(RigidBodyControl.class);
			m_PhysicsSpace = rigidBody.getPhysicsSpace();
			m_PhysicsSpace.addCollisionListener(this);
			m_PhysicsSpace.addTickListener(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
    protected void controlRender(RenderManager rm, ViewPort vp) {
        //To change body of generated methods, choose Tools | Templates.
    }
    
	@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);
		}
	}
	
	/**
	 * 
	 * @param closestBody
	 * @param pivotLocation
	 */
	private void stick(PhysicsCollisionObject closestBody, Vector3f pivotLocation) {
		
		if (closestBody.getUserObject() instanceof Node) {
			
			// transform the POI to the arrow's local coordinates
            Matrix3f closestMatrix = closestBody.getPhysicsRotationMatrix(null);
            closestMatrix.invertLocal();
            Vector3f closestOffset = closestBody.getPhysicsLocation(null);
            closestOffset.negateLocal();
            closestOffset.addLocal(pivotLocation);
            closestMatrix.mult(closestOffset, closestOffset);

            // locate the POI in the target's local coordinates
            Matrix3f mainMatrix = rigidBody.getPhysicsRotationMatrix(null);
            closestMatrix.multLocal(mainMatrix);
            float penetrationFraction = 0.35f; // for 35% penetration
            Vector3f mainOffset = ??? // tipOffset.mult(1f - 2f * penetrationFraction); //tipOffset is not defined in the snippet code

            New6Dof constraint = new New6Dof(rigidBody, (PhysicsRigidBody) closestBody,
                    mainOffset, closestOffset, Matrix3f.IDENTITY, closestMatrix, RotationOrder.XYZ);

            // disable all 3 rotational axes
            for (int axisIndex = 0; axisIndex < 3; ++axisIndex) {
                RotationMotor rMotor = constraint.getRotationMotor(axisIndex);
                rMotor.setSpringEnabled(true); // necessary, but not sure why!
                int dofIndex = axisIndex + 3;
                constraint.set(MotorParam.UpperLimit, dofIndex, 0f);
                constraint.set(MotorParam.LowerLimit, dofIndex, 0f);
            }
            constraint.setCollisionBetweenLinkedBodies(false);
            m_PhysicsSpace.addJoint(constraint);
            
//			Node gameObject = (Node) other.getUserObject();
//			gameObject.worldToLocal(hitPoint, hitPoint);
//			gameObject.attachChild(spatial);
//        	spatial.setLocalTranslation(hitPoint);
        	
//		} else if (other.getUserObject() instanceof BoneLink) {
//			BoneLink link = (BoneLink) other.getUserObject();
//        	Spatial animRoot = link.getControl().getSpatial();
//        	Node attachNode = animRoot.getControl(SkinningControl.class).getAttachmentsNode(link.boneName());
//        	System.out.println(link.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: " + closestBody.getUserObject());
		}
	}
	
	//  Destroy everything
	private void destroy() {
		rigidBody.setEnabled(false);
		m_PhysicsSpace.removeCollisionListener(this);
		m_PhysicsSpace.removeTickListener(this);
		spatial.removeFromParent();
		logger.log(Level.INFO, "Physics Object Destroyed: {0}", spatial);
	}

	@Override
	public void prePhysicsTick(PhysicsSpace space, float timeStep) {
		// TODO Auto-generated method stub
		if (hasCollided) {
			m_PhysicsSpace.removeCollisionListener(this);
			return;
		}
		
		Transform start = new Transform(rigidBody.getPhysicsLocation());
		Transform end = new Transform(rigidBody.getPhysicsLocation().add(rigidBody.getLinearVelocity().multLocal(timeStep)));
		List<PhysicsSweepTestResult> sweepTest = m_PhysicsSpace.sweepTest((ConvexShape) rigidBody.getCollisionShape(), start, end);
		
		boolean collision = false;
		
		for (PhysicsSweepTestResult result : sweepTest) {
			PhysicsCollisionObject pco = result.getCollisionObject();
			if (pco.getCollisionShape() != rigidBody.getCollisionShape()) {
				System.out.println("Colliding with: " + pco.getUserObject().toString());
				collision = true;
			}
		}
		
		if (collision) {
			rigidBody.setLinearVelocity(rigidBody.getLinearVelocity().multLocal(0.9f));
		}
	}

	@Override
	public void physicsTick(PhysicsSpace space, float timeStep) {
		// TODO Auto-generated method stub
		if (hasCollided) {
			m_PhysicsSpace.removeTickListener(this);
		}
	}

}

I can guess with another thousand unsuccessful attempts, but if you have a ready-made example you would help the whole community of newbies like me on this topic to understand how physics update loops work, how to use sweepTest and New6Dof correctly. Thanks as always for your time

1 Like

I agree it would be nice to have a complete, open-source example of arrow dynamics. That’s one of the many reasons I’m excited about your project!

Currently I’m focused on bringing More Advanced Vehicles to a release. When that’s accomplished, I have a slew of JME issues and PRs to catch up on. I’m unsure when I’ll return to the Minie Project or my tactical simulation.

While the tactical sim was never intended to be open-source, I expect to eventually extract open-source library code and examples from it. Until then, all I can provide are snippets and general advice. If you’re stuck on how to implement something, by all means ask. But don’t expect snippets pasted from another (differently organized) project to do exactly what you want.

Some excel at debugging code by visual inspection—not me! I hate reading code in a webpage, out of context. I want to browse it an an editor, execute it, and step through it in a debugger. If you want debugging help, please describe the issue, then point me to a Git hash I can clone, build, and run myself.

Regarding how physics update loops work, it seems you already know about PhysicsTickListener. I’m unsure what more there is to know!

Regarding how to use sweep tests, there’s an example in TestRbc.

For using New6Dof correctly, there’s a tutorial.

2 Likes

Sorry for my last post, maybe I was a little tired. Programming video games is difficult, sometimes there are moments of frustration, but if you share your passions with someone, you will never lose enthusiasm. I know you are working on a lot of JME improvements so I try not to waste your time with silly questions. I appreciate all your feedbacks and thanks for the reference material, I try to understand more. I will publish the code with all the new evolutions on github after doing some cleaning.

1 Like

Questions are generally welcome. I assume that for every person asking questions at the Forum, there are 9 shy people lurking to see the answers, if any.

4 Likes