Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Wrapping, capping, toggling, and slicing, oh my!
#1
Who knows, maybe everyone out there is already using these things in some form,
but I use them EVERYWHERE and maybe someone will enjoy them!  They're just simple
and common ways to manipulate values, but they will replace multiple lines of code
and make said code much easier to understand at a glance.



Code: (Select All)
function wrap(n, l1, h1) ' n is adjusted back within lower(l) and upper(h) bounds similar to mod operator
l = l1: h = h1 ' make sure h is never less than l, this also prevents division by zero
if h1 < l1 then
  l = h1: h = l1
end if
x = (l - n) / ((h - l) + 1)
if x <> int(x) then x = x + 1
wrap = n + (int(x) * ((h - l) + 1))
end function

This first function wraps a value between two values.  You pass n, and if it's beyond one
of the bounds, it wraps back around.  So say you have five menu options, and the user moves
the cursor to position 6, this will wrap it back around to 1.  Going to 0 will also wrap around
to 5.  It doesn't matter how far outside you go, and negatives are treated properly, so -256
in this example will get wrapped correctly to 4.

The bounds can be in any order, and n is usually going to be a simple addition or subtraction.
So in the case of the above menu example, [wrap(cursor + 1, 1, 5)] will move the cursor, with
the check to wrap around.  And in most menu navigation scenarios, wrapping is super nice for
the user.  So you can write the following:

if [input] = [up arrow]  then cursor = wrap(cursor - 1, 1, [max])
if [input] = [down arrow] then cursor = wrap(cursor + 1, 1, [max])

You could also use this to wrap coordinates, so the player can leave the right side of your
stage and emerge on the left, like the Pac-Man tunnel, or create a seemingly infinitely scrolling
stage, like Asteroids, which really just repeats on itself.  For the latter example, you would
wrap the coordinates of objects whenever they travel beyond the stage, and also wrap the
coordinates to within a range around the player's position, when using them to draw the screen.
The result will appear seamless, and it won't matter how fast things travel each frame.



Code: (Select All)
function wrap_a(a) ' angle a is adjusted back within 0 and 2pi, noninclusive of 2pi
x = -a / atn1(8)
if x <> int(x) then x = x + 1
wrap_a = a + (int(x) * atn1(8))
end function


function atn1(n)
atn1 = n * atn(1)
end function

This does exactly the same thing, but for angles.  Put any angle value in this function,
and it will simplify it to a positive angle less than a full circle.  Very useful when
angle values can get adjusted over and over.  Just put the function around any angle changes.

(Note that the little atn1() function is just a shorthand thing, I got tired of typing
n * atn(1) in a ton of places.  It generates an angle in radians, where the value passed in
is an eighth of a circle or 45 degrees, so atn1(8) = 2 * pi radians, or 360 degrees.)



Code: (Select All)
function plus_limit(n, p, l) ' p is added to n, but can't go past l in the direction of travel
q = n + p
if sgn(q - l) = sgn(p) then q = l
plus_limit = q
end function

This saves a lot of space capping values, and is direction-dependent.  If p is positive,
n can't go past l upward; if p is negative, n can't go past l downward.  I use this for many
things, like decaying values without going below zero, capping healing at maximum health,
trapping position coordinates at the edges of the screen, the uses are endless.  The syntax
is very clean, all packed up in one line, it replaces stuff like this:

health = health + 100
if health > max_health then health = max_health

with this:

health = plus_limit(health, 100, max_health)

The directional dependency of this is more useful than you might think at first glance,
and is a feature that simple floor and ceiling functions don't have.  For example,
you can use sgn() comparison to move something toward a goal value, and stop it if it
reaches the goal, but not snap to the goal if moving away from it.
Then you might use something like this:

x = plus_limit(x, sgn(goal - x) * speed, goal)

This can be very useful in having one value that changes instantly, and another that
constantly follows it.  In my current game project, one way I use this is to give visual
feedback about damage that was just taken.  If the player gets hit, the health is instantly
lowered, so the green health bar gets shorter.  But there's a hidden second red bar behind
it, and that red bar's value follows the health value.  If you gain health, this does
nothing, since the red bar will be shorter, but when you LOSE health, the player will see
part of the health bar become red, and instantly start shrinking, until all that's left
is the green part.

Do note that because of the dependency, if for example you are subtracting, this function
won't catch a value that was already higher than it should be.  So be careful.



Code: (Select All)
function toggle(v, p, q)
toggle = v
if v = p then toggle = q
if v = q then toggle = p
end function

This one just toggles between the given values.  However, it will not do anything
if the variable passed is not one of these two values.  Use like v = toggle(v, value1, value2).
Replaces the messier [if v = value1 then v = value2 else v = value1].  And since it's a function,
you can use it very compactly and dynamically, rather than directly manipulating variables.
Let's say you have a 1v1 RPG combat scenario, and one of the characters decided to attack.
You've calculated the damage already.  So instead of:

if turn = char1 then target = char2 else target = char1
call damage_char(target, damage)

You can write:

call damage_char(toggle(turn, char1, char2), damage)

Think of it like the binary NOT operator, except this works between any two values you want.



Code: (Select All)
function before$(t$, c$)
p = instr(t$, c$)
if p = false then p = len(t$) + 1
before$ = left$(t$, p - 1)
end function


function after$(t$, c$)
after$ = right$(t$, len(t$) - instr(t$, c$) - (len(c$) - 1))
end function


function between$(t$, c1$, c2$)
between$ = before$(after$(t$, c1$), c2$)
end function

These functions are used for slicing strings, which has been useful in developing my own scripting,
among other things.  before$ will return everything in t$ before the first instance it finds of c$,
while after$ will return everything after it.  If c$ is not found, it will simply return t$ unchanged.

An interesting application of this is to return more than one value from a function.  For example,
if you want a function to return both x and y coordinates, you can make the function's return data type
a string, pack the two values up with a comma between like "100, 50", then use before and after
functions with val() to pull the values apart outside the function.  You can also easily do this
for three-dimensional coordinates.  And it need not be limited to coordinates, you could also use
a separator character to return multiple strings packed into one string, like "Alice|Timothy|Eric".

between$, of course, just cleans up the syntax when using before$ and after$ together, which often
happens in my code.  This:

result$ = before$(after$(t$, "["), "]")

Becomes this:

result$ = between$(t$, "[", "]")
Reply
#2
(Round of applause)

The "plus_limit" is something that appeared for me a lot, I mean a lot, in my source code in any language.

The "toggle" I call "tert" in my version of Lua, could be written even simpler:

Code: (Select All)
function toggle&& (valu&&, one&&, two&&)
if valu&& = one&& then toggle&& = two&& : exit function
toggle&& = one&&
end function

It would be even better if it could support any data type.

I have one of my own that some of you might have already seen elsewhere in my source code:

Code: (Select All)
FUNCTION LeftLen$ (tx$, numchar&)
IF tx$ = "" THEN
    LeftLen$ = ""
ELSEIF numchar& > 0 THEN
    LeftLen$ = LEFT$(tx$, LEN(tx$) - numchar&)
ELSE
    LeftLen$ = tx$
END IF
END FUNCTION

It takes "numchar&" bytes away from the end of the string. Quick example, cannot think well right now due to lack of sleep:

Code: (Select All)
a$ = "Pete"
print a$; "'s "; lcase$(LeftLen$(a$, 1))

I have also written a "SEG1$()" which acts more like "string.sub()" in Lua, ie. give the starting and ending inclusive points to get a fragment of a string, and supports zero and negative values. It had to be "SEG1$()" not "SEG$()" because of "DEF SEG".

Code: (Select All)
''this works a bit differently from "string.sub()" in Lua
''for either position, 0 means the length of "astr$" and -1 means the penultimate position
FUNCTION SEG1$ (astr$, stapos AS LONG, endpos AS LONG)
DIM sp AS LONG, ep AS LONG, L AS LONG
IF astr$ = "" THEN SEG1$ = "": EXIT FUNCTION
sp = stapos
ep = endpos
L = LEN(astr$)
IF sp < 1 THEN sp = L + sp
IF ep < 1 THEN ep = L + ep
IF (sp < 1 AND ep < 1) OR (sp > L AND ep > L) THEN SEG1$ = "": EXIT FUNCTION
IF sp < 1 THEN sp = 1
IF ep < 1 THEN ep = 1
IF sp > L THEN sp = L
IF ep > L THEN ep = L
IF sp > ep THEN SWAP sp, ep
SEG1$ = MID$(astr$, sp, ep - sp + 1)
END FUNCTION

Sorry because it's not indented, blame the forum liking to eat spaces at the front of lines sometimes.
Reply
#3
Interesting stuff.

Regarding the last string-related functions, I have plans in the works to do a short tutorial video about implementing a simple scripted-behavior language in your own program, which I am now using for my big game project, it's paved the way to much cleaner code and lots of flexibility, and makes extensive use of before$() and after$(), as I mentioned.  Hopefully will get around to doing that soon.
Reply
#4
These are great!

I'll share mine in another thread.
grymmjack (gj!)
GitHubYouTube | Soundcloud | 16colo.rs
Reply
#5
(12-03-2022, 11:40 AM)mnrvovrfc Wrote: The "toggle" I call "tert" in my version of Lua, could be written even simpler:

Code: (Select All)
function toggle&& (valu&&, one&&, two&&)
if valu&& = one&& then toggle&& = two&& : exit function
toggle&& = one&&
end function

or even simpler:
Code: (Select All)
Function toggle&& (valu&&, one&&, two&&)
  toggle&& = one&& + two&& - valu&&
End Function
45y and 2M lines of MBASIC>BASICA>QBASIC>QBX>QB64 experience
Reply




Users browsing this thread: 2 Guest(s)