/* * Copyright 2013 MovingBlocks * * 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 * * http://www.apache.org/licenses/LICENSE-2.0 * * 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 org.terasology.logic.characters; import com.google.common.collect.Maps; import com.google.common.collect.Queues; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.engine.Time; import org.terasology.entitySystem.entity.EntityRef; import org.terasology.entitySystem.entity.lifecycleEvents.BeforeDeactivateComponent; import org.terasology.entitySystem.entity.lifecycleEvents.OnActivatedComponent; import org.terasology.entitySystem.event.ReceiveEvent; import org.terasology.entitySystem.systems.BaseComponentSystem; import org.terasology.registry.In; import org.terasology.entitySystem.systems.RegisterMode; import org.terasology.entitySystem.systems.RegisterSystem; import org.terasology.entitySystem.systems.UpdateSubscriberSystem; import org.terasology.logic.location.LocationComponent; import org.terasology.logic.players.LocalPlayer; import org.terasology.math.geom.Vector3f; import org.terasology.network.ClientComponent; import org.terasology.physics.engine.PhysicsEngine; import org.terasology.utilities.collection.CircularBuffer; import org.terasology.world.WorldProvider; import java.util.Deque; import java.util.Iterator; import java.util.Map; /** */ @RegisterSystem(RegisterMode.REMOTE_CLIENT) public class ClientCharacterPredictionSystem extends BaseComponentSystem implements UpdateSubscriberSystem { private static final Logger logger = LoggerFactory.getLogger(ClientCharacterPredictionSystem.class); private static final int BUFFER_SIZE = 128; @In private Time time; @In private PhysicsEngine physics; @In private WorldProvider worldProvider; @In private LocalPlayer localPlayer; private CharacterMover characterMover; private Map<EntityRef, CircularBuffer<CharacterStateEvent>> playerStates = Maps.newHashMap(); private Deque<CharacterMoveInputEvent> inputs = Queues.newArrayDeque(); private CharacterStateEvent predictedState; private CharacterStateEvent authoritiveState; private CharacterMovementSystemUtility characterMovementSystemUtility; @Override public void initialise() { characterMover = new KinematicCharacterMover(worldProvider, physics); characterMovementSystemUtility = new CharacterMovementSystemUtility(physics); } @ReceiveEvent(components = {CharacterMovementComponent.class, LocationComponent.class}) public void onCreate(final OnActivatedComponent event, final EntityRef entity) { physics.getCharacterCollider(entity); CircularBuffer<CharacterStateEvent> stateBuffer = CircularBuffer.create(BUFFER_SIZE); stateBuffer.add(createInitialState(entity)); playerStates.put(entity, stateBuffer); } @ReceiveEvent(components = {CharacterComponent.class, CharacterMovementComponent.class, LocationComponent.class}) public void onDestroy(final BeforeDeactivateComponent event, final EntityRef entity) { CharacterComponent character = entity.getComponent(CharacterComponent.class); ClientComponent controller = character.controller.getComponent(ClientComponent.class); if (controller != null && controller.local) { predictedState = null; authoritiveState = null; inputs.clear(); } physics.removeCharacterCollider(entity); playerStates.remove(entity); } @ReceiveEvent(components = {CharacterMovementComponent.class, LocationComponent.class}) public void onCharacterStateReceived(CharacterStateEvent state, EntityRef entity) { if (entity.equals(localPlayer.getCharacterEntity())) { logger.trace("Received new state, sequence number: {}, buffered input size {}", state.getSequenceNumber(), inputs.size()); playerStates.remove(entity); authoritiveState = state; Iterator<CharacterMoveInputEvent> inputIterator = inputs.iterator(); CharacterStateEvent newState = authoritiveState; while (inputIterator.hasNext()) { CharacterMoveInputEvent input = inputIterator.next(); if (input.getSequenceNumber() <= state.getSequenceNumber()) { inputIterator.remove(); } else { newState = stepState(input, newState, entity); } } logger.trace("Resultant input size {}", inputs.size()); characterMovementSystemUtility.setToState(entity, newState); // TODO: soft correct predicted state predictedState = newState; } else { playerStates.get(entity).add(state); } } @ReceiveEvent(components = {CharacterMovementComponent.class, LocationComponent.class}) public void onPlayerInput(CharacterMoveInputEvent input, EntityRef entity) { if (predictedState == null) { predictedState = createInitialState(entity); authoritiveState = predictedState; playerStates.remove(entity); } inputs.add(input); CharacterStateEvent newState = stepState(input, predictedState, entity); predictedState = newState; characterMovementSystemUtility.setToState(entity, newState); } private CharacterStateEvent createInitialState(EntityRef entity) { LocationComponent location = entity.getComponent(LocationComponent.class); return new CharacterStateEvent(time.getGameTimeInMs(), 0, location.getWorldPosition(), location.getWorldRotation(), new Vector3f(), 0, 0, MovementMode.WALKING, false); } private CharacterStateEvent stepState(CharacterMoveInputEvent input, CharacterStateEvent lastState, EntityRef entity) { return characterMover.step(lastState, input, entity); } @Override public void update(float delta) { long renderTime = time.getGameTimeInMs() - ServerCharacterPredictionSystem.RENDER_DELAY; for (Map.Entry<EntityRef, CircularBuffer<CharacterStateEvent>> entry : playerStates.entrySet()) { CharacterStateEvent previous = null; CharacterStateEvent next = null; for (CharacterStateEvent state : entry.getValue()) { if (state.getTime() <= renderTime) { previous = state; } else { next = state; break; } } if (previous != null) { if (next != null) { characterMovementSystemUtility.setToInterpolateState(entry.getKey(), previous, next, renderTime); } else { characterMovementSystemUtility.setToExtrapolateState(entry.getKey(), previous, renderTime); } } } } }