/******************************************************************************* * Copyright 2015 See AUTHORS file. * <p/> * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * <p/> * http://www.apache.org/licenses/LICENSE-2.0 * <p/> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package com.mygdx.game.objects; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.ai.fsm.DefaultStateMachine; import com.badlogic.gdx.ai.fsm.State; import com.badlogic.gdx.ai.fsm.StateMachine; import com.badlogic.gdx.ai.msg.MessageManager; import com.badlogic.gdx.ai.msg.Telegram; import com.badlogic.gdx.graphics.g3d.Model; import com.badlogic.gdx.graphics.g3d.utils.AnimationController; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Quaternion; import com.badlogic.gdx.math.Vector3; import com.badlogic.gdx.math.collision.Ray; import com.badlogic.gdx.physics.bullet.collision.btCollisionShape; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Bits; import com.mygdx.game.GameScreen; import com.mygdx.game.blender.objects.BlenderEmpty; import com.mygdx.game.settings.GameSettings; import com.mygdx.game.steerers.FollowPathSteerer; import com.mygdx.game.utilities.AnimationListener; import com.mygdx.game.utilities.Constants; import java.util.EnumMap; /** * A human character whose brain is modeled through a finite state machine. * * @author jsjolund */ public class HumanCharacter extends Ragdoll { public enum HumanArmature { RIGHT_HAND("right_hand"), LEFT_HAND("left_hand"); public final String id; HumanArmature(String id) { this.id = id; } } public enum HumanState implements State<HumanCharacter> { IDLE_STAND(true) { @Override public void enter(HumanCharacter entity) { entity.animations.animate("armature|idle_stand", -1, 1, animationListener(entity), 0.2f); } }, IDLE_CROUCH(true) { @Override public void enter(HumanCharacter entity) { entity.animations.animate("armature|idle_crouch", -1, 1, animationListener(entity), 0.2f); } }, IDLE_CRAWL(true) { @Override public void enter(HumanCharacter entity) { entity.animations.animate("armature|idle_crouch", -1, 1, animationListener(entity), 0.2f); } }, MOVE_RUN(HumanState.IDLE_STAND, 0.2f) { @Override public void enter(HumanCharacter entity) { entity.animations.animate("armature|move_run", -1, 1, animationListener(entity), 0.1f); prepareToMove(entity, HumanSteerSettings.runMultiplier); } }, MOVE_WALK(HumanState.IDLE_STAND, 0.4f) { @Override public void enter(HumanCharacter entity) { entity.animations.animate("armature|move_walk", -1, 1, animationListener(entity), 0.1f); prepareToMove(entity, 1); } }, MOVE_CROUCH(HumanState.IDLE_CROUCH, 0.5f) { @Override public void enter(HumanCharacter entity) { entity.animations.animate("armature|move_crouch", -1, 1, animationListener(entity), 0.15f); prepareToMove(entity, HumanSteerSettings.crouchMultiplier); } }, MOVE_CRAWL() {}, // Currently not used THROW() { @Override public void enter(HumanCharacter entity) { if (!entity.hasStick) { // TODO: The throw button should not be shown if human has no stick entity.stateMachine.changeState(entity.stateMachine.getPreviousState()); } else { entity.animations.animate("armature|action_throw", 1, 1, animationListener(entity), 0.1f); } } @Override public void update(HumanCharacter entity) { // Keep on updating throw animation updateAnimation(entity); AnimationListener animationListener = (AnimationListener) entity.animations.current.listener; if (animationListener.isAnimationCompleted()) { // Transition to the appropriate idle state depending on the previous state HumanState previousState = entity.stateMachine.getPreviousState(); HumanState nextState = HumanState.IDLE_STAND; if (previousState != null) { if (previousState.isMovementState()) { nextState = previousState.idleState; } else if (previousState.isIdleState()) { nextState = previousState; } } entity.stateMachine.changeState(nextState); } // This should make the human throw when the right hand is approximately at highest position. if (entity.hasStick && entity.animations.current.time > 1f) { entity.throwStick(); } } }, WHISTLE() { @Override public void enter(HumanCharacter entity) { // Stop steering and let friction and gravity arrest the entity entity.stopSteering(false); HumanState prevState = entity.stateMachine.getPreviousState(); if (prevState != null && prevState.isMovementState()) { // Save animation speed multiplier entity.animationSpeedMultiplier = prevState.animationMultiplier; } MessageManager.getInstance().dispatchMessage(Constants.MSG_GUI_CLEAR_DOG_BUTTON, entity); } @Override public void update(HumanCharacter entity) { if (entity.isMoving()) { // Keep on updating movement animation updateAnimation(entity); } else { GameScreen.screen.sounds.whistle.play(); // If the entity owns a dog send it a delayed message to emulate reaction time if (entity.dog != null) { MessageManager.getInstance().dispatchMessage(MathUtils.randomTriangular(.8f, 2f, 1.2f), null, entity.dog, Constants.MSG_DOG_LETS_PLAY); } // Transition to the appropriate idle state depending on the previous state HumanState previousState = entity.stateMachine.getPreviousState(); HumanState nextState = HumanState.IDLE_STAND; if (previousState != null) { if (previousState.isMovementState()) { nextState = previousState.idleState; } else if (previousState.isIdleState()) { nextState = previousState; } } entity.stateMachine.changeState(nextState); } } @Override public void exit(HumanCharacter entity) { // Reset entity's animation speed multiplier entity.animationSpeedMultiplier = -1; } }, DEAD() { @Override public void enter(HumanCharacter entity) { // Turn off animation entity.animations.setAnimation("armature|idle_stand", -1); entity.animations.paused = true; // Stop steering and let friction and gravity arrest the entity entity.stopSteering(false); // Set ragdoll control entity.setRagdollControl(true); // Dog owners inform the dog of the death and clear dog button if (entity.dog != null) { MessageManager.getInstance().dispatchMessage(MathUtils.randomTriangular(.8f, 2f, 1.2f), null, entity.dog, Constants.MSG_DOG_HUMAN_IS_DEAD); MessageManager.getInstance().dispatchMessage(Constants.MSG_GUI_CLEAR_DOG_BUTTON, entity); } } // @Override // public void update(HumanCharacter entity) { // } @Override public void exit(HumanCharacter entity) { entity.animations.paused = false; entity.setRagdollControl(false); // Dog owners inform the dog of the resurrection and enable whistle button if (entity.dog != null) { MessageManager.getInstance().dispatchMessage(MathUtils.randomTriangular(.8f, 1.5f), null, entity.dog, Constants.MSG_DOG_HUMAN_IS_RESURRECTED); MessageManager.getInstance().dispatchMessage(Constants.MSG_GUI_SET_DOG_BUTTON_TO_WHISTLE, entity); } } }; public final HumanState idleState; protected final float animationMultiplier; private HumanState() { this(false); } private HumanState(boolean idle) { this(null, idle ? -1 : 0); } private HumanState(HumanState idleState, float animationMultiplier) { this.idleState = idleState; this.animationMultiplier = animationMultiplier; } public boolean isMovementState() { return idleState != null; } public boolean isIdleState() { return idleState == null && animationMultiplier < 0; } protected AnimationListener animationListener(HumanCharacter entity) { AnimationListener animationListener = entity.stateAnimationListeners.get(this); if (animationListener != null) animationListener.setAnimationCompleted(false); return animationListener; } protected void prepareToMove(HumanCharacter entity, float steeringMultiplier) { entity.moveState = this; // Apply the multiplier to steering limits entity.setMaxLinearSpeed(HumanSteerSettings.maxLinearSpeed * steeringMultiplier); entity.setMaxLinearAcceleration(HumanSteerSettings.maxLinearAcceleration * steeringMultiplier); entity.setMaxAngularSpeed(HumanSteerSettings.maxAngularSpeed * steeringMultiplier); entity.setMaxAngularAcceleration(HumanSteerSettings.maxAngularAcceleration * steeringMultiplier); entity.followPathSteerer.followPathSB.setDecelerationRadius(HumanSteerSettings.decelerationRadius * steeringMultiplier); // If the entity owns a dog tell him you don't want to play and re-enable whistle if (entity.dog != null) { MessageManager.getInstance().dispatchMessage(MathUtils.randomTriangular(.8f, 2f, 1.2f), null, entity.dog, Constants.MSG_DOG_LETS_STOP_PLAYING); MessageManager.getInstance().dispatchMessage(Constants.MSG_GUI_SET_DOG_BUTTON_TO_WHISTLE, entity); } } @Override public void enter(HumanCharacter entity) { } @Override public void update(HumanCharacter entity) { if (entity.isSteering()) { if (!this.isMovementState()) { entity.stateMachine.changeState(entity.moveState); return; } } else { if (this.isMovementState()) { entity.stateMachine.changeState(this.idleState); return; } } updateAnimation(entity); } @Override public void exit(HumanCharacter entity) { } @Override public boolean onMessage(HumanCharacter entity, Telegram telegram) { return false; } protected void updateAnimation(HumanCharacter entity) { float deltaTime = Gdx.graphics.getDeltaTime(); // Use entity's animation speed multiplier, if any float multiplier = entity.animationSpeedMultiplier > 0 ? entity.animationSpeedMultiplier : animationMultiplier; if (multiplier > 0) { deltaTime *= entity.getLinearVelocity().len() * multiplier; } entity.animations.update(deltaTime * GameSettings.GAME_SPEED); } } public static class HumanSteerSettings implements SteerSettings { public static float maxLinearAcceleration = 50f; public static float maxLinearSpeed = 2f; public static float maxAngularAcceleration = 100f; public static float maxAngularSpeed = 15f; public static float idleFriction = 0.9f; public static float zeroLinearSpeedThreshold = 0.001f; public static float runMultiplier = 2f; public static float crouchMultiplier = 0.5f; public static float timeToTarget = 0.1f; public static float arrivalTolerance = 0.1f; public static float decelerationRadius = 0.5f; public static float predictionTime = 0f; public static float pathOffset = 1f; @Override public float getTimeToTarget() { return timeToTarget; } @Override public float getArrivalTolerance() { return arrivalTolerance; } @Override public float getDecelerationRadius() { return decelerationRadius; } @Override public float getPredictionTime() { return predictionTime; } @Override public float getPathOffset() { return pathOffset; } @Override public float getZeroLinearSpeedThreshold() { return zeroLinearSpeedThreshold; } @Override public float getIdleFriction() { return idleFriction; } } public final StateMachine<HumanCharacter, HumanState> stateMachine; public final AnimationController animations; public final EnumMap<HumanState, AnimationListener> stateAnimationListeners; public HumanState moveState = HumanState.MOVE_WALK; public DogCharacter dog; private float animationSpeedMultiplier = -1; public boolean selected = false; public Stick stick; public boolean hasStick; final FollowPathSteerer followPathSteerer; private final Vector3 TMP_V1 = new Vector3(); private final Vector3 TMP_V2 = new Vector3(); private final Quaternion TMP_Q = new Quaternion(); private final static float STICK_THROW_ANGLE = 45; private final static float STICK_THROW_IMPULSE_SCL = 1; public HumanCharacter(Model model, String name, Vector3 location, Vector3 rotation, Vector3 scale, btCollisionShape shape, float mass, short belongsToFlag, short collidesWithFlag, boolean callback, boolean noDeactivate, Array<BlenderEmpty> ragdollEmpties, String armatureNodeId) { super(model, name, location, rotation, scale, shape, mass, belongsToFlag, collidesWithFlag, callback, noDeactivate, ragdollEmpties, armatureNodeId, new HumanSteerSettings()); // Create path follower followPathSteerer = new FollowPathSteerer(this); // Create the animation controllers animations = new AnimationController(modelInstance); // Create animation listeners for states that need one stateAnimationListeners = new EnumMap<HumanState, AnimationListener>(HumanState.class); stateAnimationListeners.put(HumanState.THROW, new AnimationListener()); // Create the state machine stateMachine = new DefaultStateMachine<HumanCharacter, HumanState>(this); // Set the steering variables associated with default move state (walking) stateMachine.changeState(moveState); // Then make the character idle stateMachine.changeState(moveState.idleState); } public void assignDog(DogCharacter dog) { this.dog = dog; dog.human = this; } public void assignStick(Stick stick) { this.stick = stick; stick.owner = this; hasStick = true; // Remove it from the world if present, then add it after it is thrown. // FIXME: This seems to cause slight lag when throwing GameScreen.screen.engine.removeEntity(stick); } public void throwStick() { GameScreen.screen.engine.addEntity(stick); stick.body.setLinearVelocity(Vector3.Zero); stick.body.setAngularVelocity(Vector3.Zero); Vector3 rightHandPos = getBoneMidpointWorldPosition(HumanArmature.RIGHT_HAND.id, TMP_V1); stick.modelTransform.setToRotation(Vector3.Z, 90); stick.modelTransform.rotate(Constants.V3_UP, getOrientation() * MathUtils.radiansToDegrees); stick.modelTransform.setTranslation(rightHandPos); stick.body.setWorldTransform(stick.modelTransform); Vector3 humanDirection = getDirection(TMP_V1); TMP_Q.setFromAxis(TMP_V2.set(humanDirection).crs(Constants.V3_UP), STICK_THROW_ANGLE); Vector3 impulse = TMP_Q.transform(humanDirection).nor(); impulse.scl(STICK_THROW_IMPULSE_SCL); stick.body.applyImpulse(impulse, TMP_V2.set(Constants.V3_UP).scl(0.005f)); stick.hasLanded = false; hasStick = false; } public void onStickLanded() { stick.hasLanded = true; // If the entity owns a dog send it a delayed message to emulate reaction time if (dog != null) { MessageManager.getInstance().dispatchMessage(MathUtils.randomTriangular(.3f, 1.2f, .6f), null, dog, Constants.MSG_DOG_STICK_THROWN); } } public boolean isDead() { return stateMachine.getCurrentState() == HumanState.DEAD; } public boolean wantToPlay() { return stateMachine.getCurrentState() == HumanState.WHISTLE; } @Override public void update(float deltaTime) { super.update(deltaTime); stateMachine.update(); } @Override public void handleMovementRequest(Ray ray, Bits visibleLayers) { // A man only moves if is idle or already moving // For instance, the movement request will be ignored if the man is throwing the stick HumanState state = stateMachine.getCurrentState(); if (state.isIdleState() || state.isMovementState()) { followPathSteerer.calculateNewPath(ray, visibleLayers); } } public void handleStateCommand(HumanState newState) { stateMachine.changeState(newState); } public HumanState getCurrentMoveState() { return moveState; } public HumanState getCurrentIdleState() { return moveState.idleState; } }