oakmound / oak

A pure Go game engine

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Why isn't .Attatch mutative?

lolbinarycat opened this issue · comments

The first problem is that it is unclear in the documentation (and most methods in oak are mutative). The only reason I found it out is from looking at physics/attach_test.go.
The second is that you have to select down to the Vector field for the object you want to attach. So instead of spr.Attach(entity) you have spr.LayeredPoint.Vector = spr.Attach(entity).

I understand that this would probably be a breaking change, and would thus have to wait until oak v3, but at least we should update the documentation to make it clearer how to use this function. Also, while we're doing that, we should make it more clear which way the arguments should be supplied (renaming a to attachTo should be enough).

I also can't think of a single situation where I would prefer the current behavior to a mutative version.

Add to this the fact that you cannot implement this function locally, because you cannot declare methods on non-local types, and it needs to a method to take advantage of type embedding.

The documentation is straightforward to me:

// Attach takes in something for this vector to attach to and a set of
// offsets. The resulting combined vector with the offsets is then returned,
// and needs to be assigned to the calling vector.
func (v Vector) Attach(a Attachable, offsets ...float64) Vector {

Its possible this isn't the documentation that is displaying in godoc in some places?

The reason why it is implemented this way is because it must be without making each implementing type write its own Attach method that overwrites its underlying Vector. This way, anything with a Vector can also be Attached.

We can certainly look into if there's another way to implement it with an embedding struct wrapper around the Vector like collision.PhaseCollision does, but that's a heavy cost that we would pay on an entity by entity basis ideally.

Well, I figured out why I was having so many problems with the documentation. gopls running through spacemacs lsp layer only shows the first line of documentation, which is weird because it will show the entire definition (inculuding internal comments) of a eight field struct.

As for your second point, couldn't you just do something like this because of type embedding?

How would you write Attach with that design in a way that didn't involve adding a method to Player, granted that Vector is not a pointer type and has no pointer methods (which could be reconsidered, but for now lets imagine it wasn't)?

I don't think that's possible, if you want to modify a value within a function, unless the value is within the function's scope (like a global variable), you're going to need to pass a reference to that value, and, unless that value is stored in a slice, that means using a pointer.

So git history is helpful here to understand why this is the way it is now:

First, the original vectors look like you suggest:
3a400ee#diff-8cfb10d3dd0ae49a87320653cbfa587e

That meant that everywhere, we had to use *physics.Vector types.

Oak is fairly unique in its use of long embedded chains of types (e.g. Vector -> Point -> Doodad -> Reactive -> Interactive). If there's a method defined on Vector in that chain that is being called from Interactive, Interactive needs to go down several virtual steps to get to the vector function. When all of these are pointer types, you might have code that looks like this:

var in *entities.Interactive
//...
if in != nil {
    in.SetPos() // Vector method
}

But this can panic. It can panic in four different ways, if Reactive is nil, Doodad is nil, Point is nil, or Vector is nil (if embedding pointers). Making those kinds of checks everywhere is a big pain, and not much (if at all) better then just not embedding to begin with, but we have too many methods to reasonably do the latter.

In addition, when vectors are pointer types, it becomes easy to accidentally copy a vector you didn't mean to.

So we moved away from pointers for vectors, because we were struggling with how many accidental copies and panics we were running into:

4b58aa8#diff-8cfb10d3dd0ae49a87320653cbfa587e

... This was all before we had the concept of Attach at all, but by the time we added it, we were convinced this was the correct decision and didn't want to revert our whole codebase back to using vector pointers again, so we made the private x and y values inside of the vector pointers instead to still support the functionality.

In its current state, My personal recommendation would be to never try to implement a physics.Attachable (but it is possible, all the methods are exposed: https://play.golang.org/p/ypCGv2BetDq), and just embed Vectors or render.LayeredPoints or entities.Points instead. I would be interested to know what the desired behavioral change at that layer would be.

None of this is to say we couldn't re-evaluate if Vectors have to stay embedded as non-pointers, but I want to be careful as we were very intentional in this when we modified it, and the attach behavior we have is hard enough to get and use correctly that we don't regularly use it anymore. For v3 I'd be more in favor of scrapping the physics package and starting over.

You wouldn't need to store a pointer value in a struct though. All you would need is to have the attach method use a pointer receiver method.

That leads to confusion when you start taking values out of those structs and passing them around as value types, and I'd like to avoid it. One silly example (kind of short on time right now to give a better one):
https://play.golang.org/p/O419Yv_B4C7

To be perfectly clear-- I'm not opposed to changing it, but I think we should either be just using values or just using pointers.

I agree that it makes sense to overhaul the whole phys and if there is careful consideration we can probably get a pointer based implementation working nicely.

It does raise a question regarding what the path to v3 looks like its ough time frame/desired breaking changes and where the proposed changes live prior to work starting on it.

@200sc I took me a while to understand the example (I guess that's kinda the point) but it basically boils down to:

a := 0
for conditon {
  b := a
  a++
  a = b
}

but I think we should either be just using values or just using pointers.
Why? They both have their advantages, and we already aren't using just values or just pointers, as the fact that this issue was started by an unexpected functional method among a sea of mutative methods.

Also, while looking through code for examples, I realized even the mutative functions like SetX() didn't use pointer receivers.
After some more digging, I found out that oak basically already uses pointers for vectors.

type Vector struct {
	x, y       *float64
	offX, offY float64
}

notice x and y are type *float64. I find this odd, normally I would use a pointer receiver when I needed a pointer to a struct, and not just make the struct values pointers. This also makes it easier to tell which methods are mutative and which are functional, just by looking at the godoc entry, and checking if it uses a pointer receiver.

But if we're talking about overhauling the physics package, how about a way to modify the offset (or relative position, if we want to get rid of Attach altogether) of an attached node?