ZO_MovementController = ZO_Object:Subclass() --[[ Public API ]]-- function ZO_MovementController:New(...) local movementController = ZO_Object.New(self) movementController:Initialize(...) return movementController end MOVEMENT_CONTROLLER_DIRECTION_VERTICAL = 1 MOVEMENT_CONTROLLER_DIRECTION_HORIZONTAL = 2 local function GetStickMagnitude(direction) if direction == MOVEMENT_CONTROLLER_DIRECTION_VERTICAL then return DIRECTIONAL_INPUT:GetY(ZO_DI_LEFT_STICK, ZO_DI_DPAD) end return -DIRECTIONAL_INPUT:GetX(ZO_DI_LEFT_STICK, ZO_DI_DPAD) end local NUM_TICKS_TO_START_ACCELERATING = 5 local ACCELERATION_MAGNTITUDE_FACTOR = 3 local MAX_TICKS_TO_ACCEL_ACROSS = 30 function ZO_MovementController:Initialize(direction, accumulationPerSecondForChange, magnitudeQueryFunctionOverride) self.direction = direction or MOVEMENT_CONTROLLER_DIRECTION_VERTICAL self.accumulationPerSecondForChange = accumulationPerSecondForChange or 8 self.magnitudeQueryFunction = magnitudeQueryFunctionOverride or GetStickMagnitude self.totalAccumulation = 0 self.debt = 0 self.lastMagnitude = 0 self.allowAcceleration = true self.numTicksToStartAccelerating = NUM_TICKS_TO_START_ACCELERATING self.accelerationMagnitudeFactor = ACCELERATION_MAGNTITUDE_FACTOR self.numAccumulationTicks = 0 end function ZO_MovementController:SetAllowAcceleration(allowAcceleration) self.allowAcceleration = allowAcceleration end function ZO_MovementController:SetAccumulationPerSecondForChange(accumulation) self.accumulationPerSecondForChange = accumulation end function ZO_MovementController:SetNumTicksToStartAccelerating(numTicks) self.numTicksToStartAccelerating = numTicks end function ZO_MovementController:SetAccelerationMagnitudeFactor(accelerationMagnitudeFactor) self.accelerationMagnitudeFactor = accelerationMagnitudeFactor end function ZO_MovementController:IsAtMaxVelocity() if self.numAccumulationTicks > MAX_TICKS_TO_ACCEL_ACROSS then return true --if we're over the MAX_TICKS_TO_ACCEL_ACROSS we definitely aren't accelerating anymore end if self.debt == 0 then --otherwise if a snapshot of the accumulation right now is greater than the accumulation per second required, local magnitude = self:GetMagnitude()--we're already updating every frame regardless of being at max velocity local snapshotAccumulation = magnitude * GetFrameDeltaNormalizedForTargetFramerate() * self:CalculateAccelerationFactor() if snapshotAccumulation >= self.accumulationPerSecondForChange then return true elseif snapshotAccumulation <= -self.accumulationPerSecondForChange then return true end end --for sufficiently large accumulation per second for change, you can potentially get to max velocity for a several frames before --numAcculmulationTicks > MAX_TICKS_TO_ACCEL_ACROSS, ie you might get MOVE, NO, NO, MOVE, NO, MOVE, NO, NO, MOVE, NO, but short of --keeping a running history of the last several frames we can't accurately predict that we've gotten to that point. Also 20 ticks --a second is way too many to require a move return false end MOVEMENT_CONTROLLER_NO_CHANGE = 0 MOVEMENT_CONTROLLER_MOVE_NEXT = 1 MOVEMENT_CONTROLLER_MOVE_PREVIOUS = 2 local function IsChangingDirections(previousMagnitude, newMagnitude) return (previousMagnitude > 0 and newMagnitude < 0) or (previousMagnitude < 0 and newMagnitude > 0) end local ACCUMULATION_DEBT_PERCENT_AFTER_FIRST_SELECT = 1 local MIN_MAGNITUDE_FOR_ACCEL = .8 function ZO_MovementController:CheckMovement() local magnitude = self:GetMagnitude() if magnitude == 0 then self.totalAccumulation = 0 self.isMovingFromAccumulation = false self.lastMagnitude = 0 self.numAccumulationTicks = 0 else if IsChangingDirections(self.lastMagnitude, magnitude) then -- reset moving state when changing a direction self.isMovingFromAccumulation = false self.lastMagnitude = magnitude -- consume this input, some devices will "flip back" when the stick is tapped in a direction -- this will help, but not eliminate that effect return MOVEMENT_CONTROLLER_NO_CHANGE end if zo_abs(magnitude) < MIN_MAGNITUDE_FOR_ACCEL then self.numAccumulationTicks = 0 end if self.isMovingFromAccumulation then local normalizedMagnitude = magnitude * GetFrameDeltaNormalizedForTargetFramerate() if self.debt > 0 then local absNormalizedMagnitude = zo_abs(normalizedMagnitude) -- Consume it all, nothing to do here if self.debt >= absNormalizedMagnitude then self.debt = self.debt - absNormalizedMagnitude return MOVEMENT_CONTROLLER_NO_CHANGE end normalizedMagnitude = normalizedMagnitude - self.debt * zo_sign(normalizedMagnitude) self.debt = 0 end self.totalAccumulation = self.totalAccumulation + normalizedMagnitude * self:CalculateAccelerationFactor() else self.isMovingFromAccumulation = true self.numAccumulationTicks = 0 self.totalAccumulation = magnitude > 0 and self.accumulationPerSecondForChange or -self.accumulationPerSecondForChange self.debt = zo_abs(self.totalAccumulation * ACCUMULATION_DEBT_PERCENT_AFTER_FIRST_SELECT) end self.lastMagnitude = magnitude if self.totalAccumulation >= self.accumulationPerSecondForChange then self.totalAccumulation = self.totalAccumulation - self.accumulationPerSecondForChange self.numAccumulationTicks = self.numAccumulationTicks + 1 return MOVEMENT_CONTROLLER_MOVE_PREVIOUS elseif self.totalAccumulation <= -self.accumulationPerSecondForChange then self.totalAccumulation = self.totalAccumulation + self.accumulationPerSecondForChange self.numAccumulationTicks = self.numAccumulationTicks + 1 return MOVEMENT_CONTROLLER_MOVE_NEXT end end return MOVEMENT_CONTROLLER_NO_CHANGE end --[[ Private API ]]-- function ZO_MovementController:GetMagnitude() return self.magnitudeQueryFunction(self.direction) end function ZO_MovementController:CalculateAccelerationFactor() if self.allowAcceleration then return 1.0 + ZO_EaseOutQuartic(zo_clampedPercentBetween(self.numTicksToStartAccelerating, MAX_TICKS_TO_ACCEL_ACROSS, self.numAccumulationTicks)) * self.accelerationMagnitudeFactor end return 1.0 end