= Some proposed solutions
:author:
:revnumber:
:revdate: 2020/07/06


This is a user-proposed group of solutions for some or all of the exercises presented throughout the beginner tutorials (<<jme3#tutorials_for_beginners#,jme3#tutorials_for_beginners>>).
There are several ways to do them, so take what you see with a grain of salt, and actually try to do them yourself instead of jumping to the solution, for it is the best way to learn!


== Hello Update Loop


=== Exercise 1

It will spin the other way around.


=== Exercise 2

First, one must declare another Geometry, for example, a red cube:

[source,java]
----

protected Geometry redCube;

public void simpleInitApp() {
    ...

    // Creates the new cube
    Box b2 = new Box(Vector3f.ZERO, 1, 1, 1);
    redCube = new Geometry("red cube", b2);

    // For the new cube to become red colored
    Material mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat2.setColor("Color", ColorRGBA.Red);
    redCube.setMaterial(mat2);

    // To position the red cube next the other cube
    redCube.move(2, 0, 0);

    // Makes the red cube appear on screen
    rootNode.attachChild(redCube);
}

----

To have the red cube spin twice as fast as the other cube, simply make it rotate on the same axis but with double the value:

[source,java]
----

public void simpleUpdate(float tpf) {
    // make the player rotate
    player.rotate(0, 2*tpf, 0);

    // make the red cube rotate twice as fast the player
    redCube.rotate(0, 4*tpf, 0);
}

----


=== Exercise 3

One possible solution is to shrink or grow depending on current size. The cube may start by either growing or shrinking. If the cube is growing, it will start shrinking instead only when it reaches a size large enough. If the cube is shrinking, it will start growing again, only when the size is small enough. In short, the cube should switch between growing and shrinking when it reaches a specified maximum and minimum sizes. The following code is an example of this solution:

[source,java]
----

private boolean grow = true;
...
public void simpleUpdate(float tpf) {
        if (grow) {
                player.scale(1 + (3f * tpf));
        } else {
                player.scale(1 - (3f * tpf));
        }

        Vector3f s = player.getLocalScale();
        if (s.getX() > 1.2f) {
                grow = false;
        } else if (s.getX() < 0.8f) {
                grow = true;
        }
}

----

The cube starts by growing, and when it reaches a size larger than 120% its original size, it starts shrinking instead. The cube will then keep shrinking, until it reaches a size smaller than 80% its original size, which will make it start growing again, and so on.

Another approach is to switch between shrinking and growing every chosen unit of time. The tpf variable stores the time per frame rendered, so if you sum every tpf at the simpleUpdate method, you get the time passed since the game started. Using time like a stopwatch, one may grow the cube for some time, and then shrink the cube for the same amount of time, and start over again. The following code shows an example of this solution:

[source,java]
----

// Time passed
private float timeVar = 0;
...
public void simpleUpdate(float tpf) {
    timeVar += tpf;
    if (timeVar < 2) {
        player.scale(1 + tpf * 0.4f);
    } else if (timeVar < 4) {
        player.scale(1 - tpf * 0.4f);
    } else {
        timeVar = 0;
    }
}

----

The cube grows for 2 two seconds, then shrinks for another 2 seconds, and repeats indefinitely.

Another approach is to set the cube scale as the result of a sine wave. This results in a smooth repetitive oscillating scale. Note however that this approach is computational expensive due to the use of the sine function. One may create a sine wave by calculating sine as a function of time. As such, for each iteration of the simpleUpdate method, the scale is set as the result of sine as function of time plus the original cube scale. The following code shows an example of this approach:

[source,java]
----

public void simpleUpdate(float tpf) {
    float timeInSec = timer.getTimeInSeconds();
    float initScale = 1;
    float amplitude = 0.5f;
    float angularFrequency = 1;
    float scale = initScale + amplitude * FastMath.sin(timeInSec * angularFrequency);
    player.setLocalScale(scale);
}

----

The cube should repeatedly and smoothly grow and shrink and have maximum and minimum scale of 0.5 and 1.5 its original size. The following variables can change the scale behavior:

*  initScale - Sets the initial scale of cube
*  amplitude - Increases minimum and maximum scale
*  angularFrequency - Increases scale speed


=== Exercise 4

Same logic! Use a timeVar, and make the Material declaration + initialization line we had @ simpleInitApp() into only the initialization, with the Material mat; going as a global variable, so we can access it on the simpleUpdate()! Like so:

[source,java]
----

protected Material mat;

----

As global var, then the initialization cuts off the Material bit:

[source,java]
----

mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");

----

And then the simpleUpdate()

[source,java]
----

public void simpleUpdate(float tpf) {
    timeVar += tpf;
    if (timeVar > 1) {
        mat.setColor("Color", ColorRGBA.randomColor());
        timeVar= 0;
    }
}

----


=== Exercise 5

A possible solution is to change the rotation axis of player from y to x, and make it move along the z axis:

[source,java]
----

public void simpleUpdate(float tpf) {
    // make the player rotate
    player.rotate(2*tpf, 0, 0);
    player.move(0, 0, 2*tpf);
}

----

The above code should make the player roll towards the camera.


== Hello Input


=== Exercise 1

First, add the mappings for the Up and Down actions to the initKeys() method:

[source,java]
----

private void initKeys() {
    ...
    inputManager.addMapping("Up", new KeyTrigger(KeyInput.KEY_H));
    inputManager.addMapping("Down", new KeyTrigger(KeyInput.KEY_L));
    ...
    inputManager.addListener(combinedListener, new String[]{"Left", "Right", "Up", "Down", "Rotate"});
}

----

Then implement the actions in the onAnalog() method:

[source,java]
----

public void onAnalog(String name, float value, float tpf) {
    if (isRunning) {
        ...
        if (name.equals("Up")) {
            Vector3f v = player.getLocalTranslation();
            player.setLocalTranslation(v.x, v.y + value * speed, v.z);
        }
        if (name.equals("Down")) {
            Vector3f v = player.getLocalTranslation();
            player.setLocalTranslation(v.x, v.y - value * speed, v.z);
        }
    } else {
        ...
    }
}

----

This should enable cube to move upwards, if the H key is pressed, and downwards, if the L key is pressed.


=== Exercise 2

Following the proposed solution 1, add new mappings for the mouse wheel in the initKeys() method:

[source,java]
----

private void initKeys() {
    ...
    inputManager.addMapping("Up", new KeyTrigger(KeyInput.KEY_H),
                                  new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
    inputManager.addMapping("Down", new KeyTrigger(KeyInput.KEY_L),
                                    new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
    ...
}

----

Now you should be able to scroll the cube up or down with the mouse wheel.


=== Exercise 3

When the controls are user-chosen.

=== Excercise 4

[source, java]
----
flyCam.setEnabled(true);
----


== Hello Picking


=== Exercise 1

You can jump right off and obtain the hit object's material, by acessing the “closest object we previously acquired, obtain it's geometry through .getGeometry(), and then get the Geometry's material through .getMaterial(), like so:

[source,java]
----

Material g = closest.getGeometry().getMaterial();

----

It's the same as going through the two steps hinted in the tips: `Geometry g = closest.getGeometry(); Material material = g.getMaterial();`
Finally, you need only add this line: `material.setColor(“Color, ColorRGBA.randomColor())` , which will change the material from the hit object to a random color!

The lines can be added anywhere within the `if (results.size() &gt; 0)` block, after declaring the closest object. End result is as so:

[source,java]
----

Material material = closest.getGeometry().getMaterial();
material.setColor("Color", ColorRGBA.randomColor());

----


=== Exercise 2

First of all, we need some light shed to make the model visible! Add a simple DirectionalLight like previously showed.
Then, declare a `Spatial golem` variable outside of methods. Then initialize golem to load his model:

[source,java]
----

golem = assetManager.loadModel("Models/Oto/Oto.mesh.xml");

----

Now we need him to show up! So we need to attach him: but the rootNode won't do, because we're checking collision with it's child, the shootables node! So we attach it to shootables!

[source,java]
----

shootables.attachChild(golem);

----


=== Exercise 3

Here is my code, it works and it is well commented.

[source,java]
----

package jme3test.helloworld;

import com.jme3.app.SimpleApplication;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.light.DirectionalLight;
import com.jme3.material.MatParam;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Sphere;
import com.jme3.system.SystemListener;

public class HelloPicking extends SimpleApplication
{

    public static void main(String[] args)
    {
	HelloPicking app = new HelloPicking();
	app.start();
    }
    private Node shootables;
    private Node inventory;
    private Vector3f oldPosition;

    @Override
    public void simpleInitApp()
    {
	initCrossHairs();
	initKeys();
	shootables = new Node("Shootables");
	inventory = new Node("Inventory");
	guiNode.attachChild(inventory);
	// add a light to the HUD so we can see the robot
	DirectionalLight sun = new DirectionalLight();
	sun.setDirection(new Vector3f(0, 0, -1.0f));
	guiNode.addLight(sun);
	rootNode.attachChild(shootables);
	shootables.attachChild(makeCube("a Dragon", -2f, 0f, 1f));
	shootables.attachChild(makeCube("a tin can", 1f, -2f, 0f));
	shootables.attachChild(makeCube("the Sheriff", 0f, 1f, -2f));
	shootables.attachChild(makeCube("the Deputy", 1f, 0f, -4f));
	shootables.attachChild(makeFloor());
	shootables.attachChild(makeCharacter());
    }
    private ActionListener actionListener = new ActionListener()
    {
	public void onAction(String name, boolean keyPressed, float tpf)
	{
	    if (name.equals("Shoot") && !keyPressed)
	    {
		if (!inventory.getChildren().isEmpty())
		{
		    Spatial s1 = inventory.getChild(0);
		    // scale back
		    s1.scale(.02f);
		    s1.setLocalTranslation(oldPosition);
		    inventory.detachAllChildren();
		    shootables.attachChild(s1);
		}
		else
		{
		    CollisionResults results = new CollisionResults();
		    Ray ray = new Ray(cam.getLocation(), cam.getDirection());
		    shootables.collideWith(ray, results);

		    if (results.size() > 0)
		    {
			CollisionResult closest = results.getClosestCollision();
			Spatial s = closest.getGeometry();
			// we cheat Model differently with simple Geometry
			// s.parent is Oto-ogremesh when s is Oto_geom-1 and that is what we need
			if (s.getName().equals("Oto-geom-1"))
			{
			    s = s.getParent();
			}
			// It's important to get a clone or otherwise it will behave weird
			oldPosition = s.getLocalTranslation().clone();
			shootables.detachChild(s);
			inventory.attachChild(s);
			// make it bigger to see on the HUD
			s.scale(50f);
			// make it on the HUD center
			s.setLocalTranslation(settings.getWidth() / 2, settings.getHeight() / 2, 0);
		    }
		}
	    }
	}
    };

    private void initKeys()
    {
	inputManager.addMapping("Shoot",
				new KeyTrigger(KeyInput.KEY_SPACE),
				new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
	inputManager.addListener(actionListener, "Shoot");
    }
    protected Geometry makeCube(String name, float x, float y, float z)
    {
	Box box = new Box(1, 1, 1);
	Geometry cube = new Geometry(name, box);
	cube.setLocalTranslation(x, y, z);
	Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
	mat1.setColor("Color", ColorRGBA.randomColor());
	cube.setMaterial(mat1);
	return cube;
    }
    protected Geometry makeFloor()
    {
	Box box = new Box(15, .2f, 15);
	Geometry floor = new Geometry("the Floor", box);
	floor.setLocalTranslation(0, -4, -5);
	Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
	mat1.setColor("Color", ColorRGBA.Gray);
	floor.setMaterial(mat1);
	return floor;
    }
    protected void initCrossHairs()
    {
	setDisplayStatView(false);
	guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
	BitmapText ch = new BitmapText(guiFont, false);
	ch.setSize(guiFont.getCharSet().getRenderedSize() * 2);
	ch.setText("+");
	ch.setLocalTranslation(
		settings.getWidth() / 2 - ch.getLineWidth() / 2, settings.getHeight() / 2 + ch.getLineHeight() / 2, 0);
	guiNode.attachChild(ch);
    }
    protected Spatial makeCharacter()
    {
	Spatial golem = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
	golem.scale(0.5f);
	golem.setLocalTranslation(-1.0f, -1.5f, -0.6f);
	System.out.println("golem.locaoTranslation:" + golem.getLocalTranslation());
	DirectionalLight sun = new DirectionalLight();
	sun.setDirection(new Vector3f(0, 0, -1.0f));
	golem.addLight(sun);
	return golem;
    }
}

----
