0

Short and simple, I just don't get why this works the way it does:

using UnityEngine;
using System.Collections.Generic;

// this struct holds data for a single 'shake' source
public struct Shake
{
    public float AmountX;
    public float AmountY;
    public float Duration;

    private float percentageComplete;
    public float PercentageComplete { get { return percentageComplete; } }

    public Shake(float _amountX, float _amountY, float _duration)
    {
        AmountX = _amountX;
        AmountY = _amountY;
        Duration = _duration;
        percentageComplete = 0f;
        Debug.Log("wtf");
    }

    public void Update()
    {
        Debug.Log("a " + percentageComplete);
        percentageComplete += Time.deltaTime;
        Debug.Log("b " + percentageComplete);

        AmountX = Mathf.Lerp(AmountX, 0f, percentageComplete);
        AmountY = Mathf.Lerp(AmountY, 0f, percentageComplete);

    }
}

// This class uses that data to implement the shakes on a game camera
public class CameraShake : MonoBehaviour 
{
    // components
    private Transform myTransform;

    // vars
    private List<Shake> shakes;

    private float totalShakeX;
    private float totalShakeY;

    private void Awake()
    {
        myTransform = transform;

        shakes = new List<Shake>();
    }

    private void Update() 
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            AddShake(new Shake(.1f, .1f, 1f));
        }

        totalShakeX = 0f;
        totalShakeY = 0f;

        for (int i = 0; i < shakes.Count; i++)
        {
            shakes[i].Update();
            Debug.Log("c " + shakes[i].PercentageComplete);

            totalShakeX += shakes[i].AmountX;
            totalShakeY += shakes[i].AmountY;

            if (shakes[i].PercentageComplete >= 1f)
            {
                shakes.RemoveAt(i);
                i--;
            }
        }

        myTransform.position += new Vector3(totalShakeX, 0f, totalShakeY);
    }

    public void AddShake(Shake shake)
    {
        shakes.Add(shake);
    }
}

When I run this code (using Unity3D game engine) the struct Shake does not log the correct values.

Percentage complete logs these values (every frame): a 0 b 0.001 (or hereabouts, its Time.deltaTime, the maount of time it took to process the previous frame) c 0

What gives? Why is percentageComplete resetting to 0 every frame?

If I change Shake into a class instead of a struct it works fine.

Casper
  • 429
  • 1
  • 6
  • 14

3 Answers3

1

Maybe it's because structs are value types, so copying a Shake object into the list copies it by value, not by reference.

user1610015
  • 6,561
  • 2
  • 15
  • 18
  • Yeah everything is working correctly OP should read about what struct and class actually do http://msdn.microsoft.com/en-us/library/ms173109.aspx. – ClassicThunder Oct 23 '12 at 18:31
  • Hmmmm.. I don't think so, because the game engine (Unity3D) includes a struct/value type for Vectors called Vector3. These work just fine when added to a List<> using the Add method and keyword new... Like so: listOfVector3s.Add(new Vector3(0f, 0f, 0f)); – Casper Oct 23 '12 at 18:32
  • @Draknir Yeah I'm not too sure what the problem is, that's why I said "maybe". But I don't see any other reason why percentageComplete would reset to zero. – user1610015 Oct 23 '12 at 18:37
  • @Draknir: **Immutable** structs are not a problem. – SLaks Oct 23 '12 at 20:20
1

You're using an [evil mutable struct]http://stackoverflow.com/questions/441309/why-are-mutable-structs-evil).

When you call a method on a struct from a list (or from anyhitng other than a strongly-typed field or local variable), it will call the method on a new copy of the struct, without affecting the original.

SLaks
  • 868,454
  • 176
  • 1,908
  • 1,964
1

C# does not allow a property access to combine a read and a write, which means an access to a property of a structure type will be considered a read-only access. Unfortunately, C# provides no means of indicating whether a struct method will modify this. Consequently, an attempt to invoke a struct method which modifies this on a read-only struct will compile fine, but it won't actually work.

To avoid this problem, if your struct needs to define a method like Update which modifies a struct in-place, you should change it to be something like:

public static void Update(ref Shake it)
{
    it.percentageComplete += it.Time.deltaTime;
    it.AmountX = Mathf.Lerp(it.AmountX, 0f, it.percentageComplete);
    it.AmountY = Mathf.Lerp(it.AmountY, 0f, it.percentageComplete);
}

The compiler would then be able to squawk, correctly, if you were to attempt something like: Shake.Update(ref shakes[i]); when shakes is a List<Shake>, but would allow a construct like var temp = shakes[i]; Shake.Update(ref temp); shakes[i] = temp;, or would allow the first construct (and it would work), if shakes had been a Shake[] rather than a List<Shake>.

Value types have different semantics from reference types. Sometimes these different semantics can be useful. The semantic differences are more apparent with value types that allow convenient piece-wise mutation than with those that do not, and some people who believe all types should behave identically denounce them as evil for that reason, but I disagree with such thinking. A struct which exposes all its fields, and which has no methods that mutate this, will behave like every other struct meeting that description (save for the precise names and types of its fields). Such behavior is unlike that of any reference type, but that's what makes it useful. By contrast, while structs with private fields may be coded so as to "pretend" to be immutable, and to eliminate the semantic advantages of value types, mutable storage locations of non-trivial struct types will always hold mutable struct instances, regardless of whether the types pretend to be immutable. IMHO, it's better for value types to advertise themselves as what they are, than to pretend to behave as something they're not.

supercat
  • 77,689
  • 9
  • 166
  • 211