Пример #1
0
        protected HitObject GetMostValidObject()
        {
            // The most optimal lookup case we have is when an object is alive. There are usually very few alive objects so there's no drawbacks in attempting this lookup each time.
            var hitObject = hitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true)?.HitObject;

            // In the case a next object isn't available in drawable form, we need to do a somewhat expensive traversal to get a valid sound to play.
            if (hitObject == null)
            {
                // This lookup can be skipped if the last entry is still valid (in the future and not yet hit).
                if (fallbackObject == null || fallbackObject.Result?.HasResult == true)
                {
                    // We need to use lifetime entries to find the next object (we can't just use `hitObjectContainer.Objects` due to pooling - it may even be empty).
                    // If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager.
                    fallbackObject = hitObjectContainer.Entries
                                     .Where(e => e.Result?.HasResult != true)
                                     .OrderBy(e => e.HitObject.StartTime)
                                     .FirstOrDefault();

                    // In the case there are no unjudged objects, the last hit object should be used instead.
                    fallbackObject ??= hitObjectContainer.Entries.LastOrDefault();
                }

                hitObject = fallbackObject?.HitObject;
            }

            return(hitObject);
        }
Пример #2
0
        protected override void RemoveDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
        {
            base.RemoveDrawable(entry, drawable);

            drawable.DefaultsApplied -= invalidateHitObject;
            layoutComputed.Remove(drawable);
        }
Пример #3
0
        protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
        {
            base.AddDrawable(entry, drawable);

            invalidateHitObject(drawable);
            drawable.DefaultsApplied += invalidateHitObject;
        }
Пример #4
0
 /// <summary>
 /// Creates a new <see cref="DrawableHitObject"/>.
 /// </summary>
 /// <param name="initialHitObject">
 /// The <see cref="HitObject"/> to be initially applied to this <see cref="DrawableHitObject"/>.
 /// If <c>null</c>, a hitobject is expected to be later applied via <see cref="Apply(osu.Game.Rulesets.Objects.HitObjectLifetimeEntry)"/> (or automatically via pooling).
 /// </param>
 protected DrawableHitObject([CanBeNull] HitObject initialHitObject = null)
 {
     if (initialHitObject != null)
     {
         lifetimeEntry = new SyntheticHitObjectEntry(initialHitObject);
         ensureEntryHasResult();
     }
 }
Пример #5
0
 public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry)
 {
     if (lifetimeEntry != null)
     {
         Apply(lifetimeEntry);
     }
     else
     {
         Apply(hitObject);
     }
 }
Пример #6
0
        /// <summary>
        /// Removes the currently applied <see cref="HitObject"/>
        /// </summary>
        private void free()
        {
            if (!hasHitObjectApplied)
            {
                return;
            }

            StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
            if (HitObject is IHasComboInformation combo)
            {
                comboIndexBindable.UnbindFrom(combo.ComboIndexBindable);
            }
            samplesBindable.UnbindFrom(HitObject.SamplesBindable);

            // Changes in start time trigger state updates. When a new hitobject is applied, OnApply() automatically performs a state update anyway.
            StartTimeBindable.ValueChanged -= onStartTimeChanged;

            // When a new hitobject is applied, the samples will be cleared before re-populating.
            // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply().
            samplesBindable.CollectionChanged -= onSamplesChanged;

            // Release the samples for other hitobjects to use.
            if (Samples != null)
            {
                Samples.Samples = null;
            }

            if (nestedHitObjects.IsValueCreated)
            {
                foreach (var obj in nestedHitObjects.Value)
                {
                    obj.OnNewResult            -= onNewResult;
                    obj.OnRevertResult         -= onRevertResult;
                    obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
                }

                nestedHitObjects.Value.Clear();
                ClearNestedHitObjects();
            }

            HitObject.DefaultsApplied -= onDefaultsApplied;

            OnFree();

            HitObject       = null;
            ParentHitObject = null;
            Result          = null;
            lifetimeEntry   = null;

            clearExistingStateTransforms();

            hasHitObjectApplied = false;
        }
Пример #7
0
        private void removeDrawable(HitObjectLifetimeEntry entry)
        {
            Debug.Assert(drawableMap.ContainsKey(entry));

            var drawable = drawableMap[entry];

            drawable.OnNewResult    -= onNewResult;
            drawable.OnRevertResult -= onRevertResult;
            drawable.OnKilled();

            drawableMap.Remove(entry);

            unbindStartTime(drawable);
            RemoveInternal(drawable);

            HitObjectUsageFinished?.Invoke(entry.HitObject);
        }
Пример #8
0
        private void addDrawable(HitObjectLifetimeEntry entry)
        {
            Debug.Assert(!drawableMap.ContainsKey(entry));

            var drawable = pooledObjectProvider.GetPooledDrawableRepresentation(entry.HitObject);

            if (drawable == null)
            {
                throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}.");
            }

            drawable.OnNewResult    += onNewResult;
            drawable.OnRevertResult += onRevertResult;

            bindStartTime(drawable);
            AddInternal(drawableMap[entry] = drawable, false);

            HitObjectUsageBegan?.Invoke(entry.HitObject);
        }
Пример #9
0
        private void removeDrawable(HitObjectLifetimeEntry entry)
        {
            Debug.Assert(drawableMap.ContainsKey(entry));

            var drawable = drawableMap[entry];

            // OnKilled can potentially change the hitobject's result, so it needs to run first before unbinding.
            drawable.OnKilled();
            drawable.OnNewResult    -= onNewResult;
            drawable.OnRevertResult -= onRevertResult;

            drawableMap.Remove(entry);

            OnRemove(drawable);
            unbindStartTime(drawable);
            RemoveInternal(drawable);

            HitObjectUsageFinished?.Invoke(entry.HitObject);
        }
Пример #10
0
        public void TestCorrectHitObject()
        {
            HitObjectLifetimeEntry nextObjectEntry = null;

            AddAssert("no alive objects", () => getNextAliveObject() == null);

            AddAssert("check initially correct object", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[0]);

            AddUntilStep("get next object", () =>
            {
                var nextDrawableObject = getNextAliveObject();

                if (nextDrawableObject != null)
                {
                    nextObjectEntry = nextDrawableObject.Entry;
                    InputManager.MoveMouseTo(nextDrawableObject.ScreenSpaceDrawQuad.Centre);
                    return(true);
                }

                return(false);
            });

            AddUntilStep("hit first hitobject", () =>
            {
                InputManager.Click(MouseButton.Left);
                return(nextObjectEntry.Result.HasResult);
            });

            AddAssert("check correct object after hit", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[1]);

            AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[2]);
            AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);

            AddUntilStep("no alive objects", () => getNextAliveObject() == null);
            AddAssert("check correct object after none alive", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);
        }
Пример #11
0
        /// <summary>
        /// Applies a new <see cref="HitObject"/> to be represented by this <see cref="DrawableHitObject"/>.
        /// </summary>
        /// <param name="hitObject">The <see cref="HitObject"/> to apply.</param>
        /// <param name="lifetimeEntry">The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of <paramref name="hitObject"/>.</param>
        public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry)
        {
            free();

            HitObject = hitObject ?? throw new InvalidOperationException($"Cannot apply a null {nameof(HitObject)}.");

            this.lifetimeEntry = lifetimeEntry;

            if (lifetimeEntry != null)
            {
                // Transfer lifetime from the entry.
                LifetimeStart = lifetimeEntry.LifetimeStart;
                LifetimeEnd   = lifetimeEntry.LifetimeEnd;

                // Copy any existing result from the entry (required for rewind / judgement revert).
                Result = lifetimeEntry.Result;
            }
            else
            {
                LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
            }

            // Ensure this DHO has a result.
            Result ??= CreateResult(HitObject.CreateJudgement())
            ?? throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");

            // Copy back the result to the entry for potential future retrieval.
            if (lifetimeEntry != null)
            {
                lifetimeEntry.Result = Result;
            }

            foreach (var h in HitObject.NestedHitObjects)
            {
                var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h, this);
                var drawableNested       = pooledDrawableNested
                                           ?? CreateNestedHitObject(h)
                                           ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");

                // Only invoke the event for non-pooled DHOs, otherwise the event will be fired by the playfield.
                if (pooledDrawableNested == null)
                {
                    OnNestedDrawableCreated?.Invoke(drawableNested);
                }

                drawableNested.OnNewResult            += onNewResult;
                drawableNested.OnRevertResult         += onRevertResult;
                drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState;

                // This is only necessary for non-pooled DHOs. For pooled DHOs, this is handled inside GetPooledDrawableRepresentation().
                // Must be done before the nested DHO is added to occur before the nested Apply()!
                drawableNested.ParentHitObject = this;

                nestedHitObjects.Value.Add(drawableNested);
                AddNestedHitObject(drawableNested);
            }

            StartTimeBindable.BindTo(HitObject.StartTimeBindable);
            StartTimeBindable.BindValueChanged(onStartTimeChanged);

            if (HitObject is IHasComboInformation combo)
            {
                comboIndexBindable.BindTo(combo.ComboIndexBindable);
            }

            samplesBindable.BindTo(HitObject.SamplesBindable);
            samplesBindable.BindCollectionChanged(onSamplesChanged, true);

            HitObject.DefaultsApplied += onDefaultsApplied;

            OnApply();
            HitObjectApplied?.Invoke(this);

            // If not loaded, the state update happens in LoadComplete().
            if (IsLoaded)
            {
                if (Result.IsHit)
                {
                    updateState(ArmedState.Hit, true);
                }
                else if (Result.HasResult)
                {
                    updateState(ArmedState.Miss, true);
                }
                else
                {
                    updateState(ArmedState.Idle, true);
                }
            }

            hasHitObjectApplied = true;
        }
Пример #12
0
        /// <summary>
        /// Applies a new <see cref="HitObject"/> to be represented by this <see cref="DrawableHitObject"/>.
        /// </summary>
        /// <param name="hitObject">The <see cref="HitObject"/> to apply.</param>
        /// <param name="lifetimeEntry">The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of <paramref name="hitObject"/>.</param>
        public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry)
        {
            free();

            HitObject = hitObject ?? throw new InvalidOperationException($"Cannot apply a null {nameof(HitObject)}.");

            this.lifetimeEntry = lifetimeEntry;

            if (lifetimeEntry != null)
            {
                // Transfer lifetime from the entry.
                LifetimeStart = lifetimeEntry.LifetimeStart;
                LifetimeEnd   = lifetimeEntry.LifetimeEnd;

                // Copy any existing result from the entry (required for rewind / judgement revert).
                Result = lifetimeEntry.Result;
            }
            else
            {
                LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
            }

            // Ensure this DHO has a result.
            Result ??= CreateResult(HitObject.CreateJudgement())
            ?? throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");

            // Copy back the result to the entry for potential future retrieval.
            if (lifetimeEntry != null)
            {
                lifetimeEntry.Result = Result;
            }

            foreach (var h in HitObject.NestedHitObjects)
            {
                var drawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h)
                                     ?? CreateNestedHitObject(h)
                                     ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");

                drawableNested.OnNewResult            += onNewResult;
                drawableNested.OnRevertResult         += onRevertResult;
                drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState;

                nestedHitObjects.Value.Add(drawableNested);
                AddNestedHitObject(drawableNested);

                drawableNested.OnParentReceived(this);
            }

            StartTimeBindable.BindTo(HitObject.StartTimeBindable);
            StartTimeBindable.BindValueChanged(onStartTimeChanged);

            if (HitObject is IHasComboInformation combo)
            {
                comboIndexBindable.BindTo(combo.ComboIndexBindable);
            }

            samplesBindable.BindTo(HitObject.SamplesBindable);
            samplesBindable.BindCollectionChanged(onSamplesChanged, true);

            HitObject.DefaultsApplied += onDefaultsApplied;

            OnApply(hitObject);
            HitObjectApplied?.Invoke(this);

            // If not loaded, the state update happens in LoadComplete(). Otherwise, the update is scheduled to allow for lifetime updates.
            if (IsLoaded)
            {
                Schedule(() => updateState(ArmedState.Idle, true));
            }

            hasHitObjectApplied = true;
        }
Пример #13
0
 public bool Remove(HitObjectLifetimeEntry entry) => lifetimeManager.RemoveEntry(entry);
Пример #14
0
 public void Add(HitObjectLifetimeEntry entry) => lifetimeManager.AddEntry(entry);
Пример #15
0
        /// <summary>
        /// Applies a new <see cref="HitObjectLifetimeEntry"/> to be represented by this <see cref="DrawableHitObject"/>.
        /// </summary>
        public void Apply([NotNull] HitObjectLifetimeEntry newEntry)
        {
            free();

            lifetimeEntry = newEntry;

            // LifetimeStart is already computed using HitObjectLifetimeEntry's InitialLifetimeOffset.
            // We override this with DHO's InitialLifetimeOffset for a non-pooled DHO.
            if (newEntry is SyntheticHitObjectEntry)
            {
                lifetimeEntry.LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
            }

            LifetimeStart = lifetimeEntry.LifetimeStart;
            LifetimeEnd   = lifetimeEntry.LifetimeEnd;

            ensureEntryHasResult();

            foreach (var h in HitObject.NestedHitObjects)
            {
                var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h, this);
                var drawableNested       = pooledDrawableNested
                                           ?? CreateNestedHitObject(h)
                                           ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");

                // Only invoke the event for non-pooled DHOs, otherwise the event will be fired by the playfield.
                if (pooledDrawableNested == null)
                {
                    OnNestedDrawableCreated?.Invoke(drawableNested);
                }

                drawableNested.OnNewResult            += onNewResult;
                drawableNested.OnRevertResult         += onRevertResult;
                drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState;

                // This is only necessary for non-pooled DHOs. For pooled DHOs, this is handled inside GetPooledDrawableRepresentation().
                // Must be done before the nested DHO is added to occur before the nested Apply()!
                drawableNested.ParentHitObject = this;

                nestedHitObjects.Add(drawableNested);
                AddNestedHitObject(drawableNested);
            }

            StartTimeBindable.BindTo(HitObject.StartTimeBindable);
            StartTimeBindable.BindValueChanged(onStartTimeChanged);

            if (HitObject is IHasComboInformation combo)
            {
                comboIndexBindable.BindTo(combo.ComboIndexBindable);
            }

            samplesBindable.BindTo(HitObject.SamplesBindable);
            samplesBindable.BindCollectionChanged(onSamplesChanged, true);

            HitObject.DefaultsApplied += onDefaultsApplied;

            OnApply();
            HitObjectApplied?.Invoke(this);

            // If not loaded, the state update happens in LoadComplete().
            if (IsLoaded)
            {
                if (Result.IsHit)
                {
                    updateState(ArmedState.Hit, true);
                }
                else if (Result.HasResult)
                {
                    updateState(ArmedState.Miss, true);
                }
                else
                {
                    updateState(ArmedState.Idle, true);
                }
            }

            hasEntryApplied = true;
        }