7
\$\begingroup\$

My apologies if I've misunderstood the term 'unit vector' or misapplied it in concept, but I think that is the term for an object's heading when expressed as [x, y] when x and y are a value between -1 and 1; so if an object were moving 'south' on a computer screen, it would have a unit vector of

[0, 1]

...right?

Anyway, I've been using this concept to move my objects and rotate their images (when needed), but I think the math I'm using to determine the unit vector is... suboptimal.

def set_heading(self, goal):
    """Uses a 'goal' (x, y) to set the object's heading.
    Returns list of 0s and 1s for 'straight' headings.
    Diagonal headings are + and/or - math.sqrt(2)/2.
    """
    vals = [a - b for a, b in zip(goal, self.pos)]
    self.heading = [i / abs(i) if i != 0 else 0 for i in vals]
    if 0 not in self.heading:
        self.heading = [i * (sqrt(2)/2) for i in self.heading]

(If you're not familiar with Python ternary syntax, the second line contains i / abs(i) if i != 0 else 0 which is Python's way of saying x if Condition else y as opposed to the usual Condition? x : else y syntax in other languages. Basically I'm just trying to avoid dividing by zero!)

So as you can see, this method of doing this sort of 'locks' the object into eight directions; if a 0 is not present in the heading, the method assumes that we're traveling diagonally and so multiplies the values by sqrt(2) / 2 to ensure that the object doesn't travel faster than it should when moving diagonally. In this way I can move the object by simply adding the result of the unit vector times its speed to its current x and y coordinates.

def move(self):
    """Moves the object by changing self.pos."""
    self.pos = [a + (b * self.speed) for a, b in zip(self.pos, self.heading)]

I can't help but feel like the method for getting the unit vector (if that's what it's correctly called) is sophomoric. Especially since it is locked into eight directions - it's fine for the little project I'm working with currently, but I'm not sure of the right method for getting a more precise unit vector. What is the correct method for doing so - and is it more or less performant than the weirdness I have come up with independently?

\$\endgroup\$
  • \$\begingroup\$ I don't have the time to write a fully fleshed out answer, but basically what you're supposed to do is substracting self from goal vectors, which gives you the vector between those two points. You then normalize it (i.e. divide by its magnitude), which results in the unit vector you want. Unit vector (or often "normalized vector") is not defined by having values between 0 and 1, but by having a length of exactly 1 unit - that both values end up being between 0 and 1 just follow from that. \$\endgroup\$ – Christian Sep 30 '14 at 13:34
  • \$\begingroup\$ right. sorry, i guess i thought that understanding was expressed in the results of the method. A method that returns a diagonal (like 'southeast' for example) would return [.707, .707], rounded for sanity here b/c i'm not typing out the 15+ digits of sqrt(2)/2. But it would return coordinates which are 1 length away from [0, 0] which could be multiplied by the object's speed attribute to put it where it's headed \$\endgroup\$ – Stick Sep 30 '14 at 14:16
12
\$\begingroup\$

A Unit Vector is of length 1.

A given vector can be converted to a unit vector by dividing it by it's magnitude. (With the exception of course that a zero length vector can not be converted).

Note that magnitude can be calculated using the Pythagorean theorem

For example if a vector has components: (x, y, z)

magnitude = sqrt( x2+ y2+ z2)

unit vector = ( x / magnitude , y / magnitude, z / magnitude )


Annan raises an interesting point that for vectors with a magnitude larger than math.sqrt(sys.float_info.max) (approximately 1.3e+154 on my machine) this will fail when using floats in the normal manner. In this case a workaround using longs to hold the working values and then finding the square root manually will be functional but relatively slow.

def isqrt(n):
    x = n
    y = (x + 1) // 2
    while y < x:
        x = y
        y = (x + n // x) // 2
    return x

def large_magnitude(x, y, z):
    long_magnitude_squared = pow(long(x), 2) + pow(long(y), 2) + pow(long(z), 2)
    return float(isqrt(long_magnitude_squared))

Credit for this implementation of Newton's Method goes to user448810.

\$\endgroup\$
  • \$\begingroup\$ ah, this is nice. Inevitable followup then -- would I ideally work to get the call to sqrt() out of the logic altogether (and thus no longer be working strictly with unit vectors) or is this an acceptable use for it \$\endgroup\$ – Stick Sep 30 '14 at 14:19
  • 2
    \$\begingroup\$ Since you're working in python, I'd suggest - just use the sqrt and don't worry about it. \$\endgroup\$ – amitp Sep 30 '14 at 15:11
  • \$\begingroup\$ hah, well - that's fine too, can I ask what that's meant to mean though? I hazard a guess; that Python's slow and that there's no ultimate upshot in trying to get around sqrt(). Which is fine -- I'm not coding for a living or anything :D \$\endgroup\$ – Stick Sep 30 '14 at 18:46
  • 1
    \$\begingroup\$ @Stick Because Python is an interpreted language, so there is some overhead that is roughly equivalent to the number of commands executed. Internally Python use well optimised machine code, so the process of calculating the square root is not subject to this overhead. \$\endgroup\$ – aaaaaaaaaaaa Sep 30 '14 at 19:24
  • 1
    \$\begingroup\$ @Annan I have added a workaround that will handle magnitudes up to sys.float_info.max (approximately 1.7e+308 on my machine) beyond that you will probably want to use a long based vector anyway. \$\endgroup\$ – Kelly Thomas Feb 2 '17 at 9:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.