This will generate random, endless rolling hills.
It scrolls into/out of view at a specified speed.
Points that scroll off the screen (to the left) are removed from the array.
When a new hill is needed, it creates one (toggling the direction up or down).
It uses this new hill (really just a height value) and slices up the hill horizontally into segments.
It will scroll this new hill into view, then repeat endlessly.
In memory, the array should only have enough segment slices to fill the screen, plus a bit before, and the next hill yet to be shown.
Controls:
[<-] - Cursor Left - Decrease Speed (by 0.05)
[->] - Cursor Right - Increase Speed (by 0.05)
[1] - Change display mode to 'MODE_TEXTURE'
[2] - Change display mode to 'MODE_TEXTURE_WIRE'
[3] - Change display mode to 'MODE_WIRE'
[4] - Change display mode to 'MODE_POLYGON'
[5] - Change display mode to 'MODE_SOLID'
(The display modes are explained in the source code, nothing much really, just for debugging)
I'm not a graphics guy, so I just whipped up a sample texture to demo this.
If you notice the light brown bottom dirt border, and if you switch to 'MODE_TEXTURE_WIRE', the middle line follows this dirt border.
(This is configurable in code by a percentage - it is set to 25%)
While this may not affect the display too much, you can use the mid-line coordinates to wire up a physics engine and use that line for the colliders.
(It does affect the display a bit, kinda confusing, check out the code where ever I used that variable).
I tried commenting the code, but I ran out of time. May update it later.
I attached the png texture I used.
And I zipped up the project too.
I hope it works, I'm in a rush!
Notes:
- a 'hill' is just the x/y coordinates of a peak or valley
- segments are how I slice up the hill and use a COSINE function to provide the rolling effect
- when the texture runs out, I just start it over again, but it may not have fully displayed the full texture, so when it joins with the next segment, it may not join up nicely - not sure how to get around this
- play with the hill settings, but if it is too much of a slope, the polygons will overlap, I could provide a work around, but it is simpler to just live with this constraint
RollingHills.gbas
types.gbas
[EDIT] Fixed most / all bugs. Updated demo for parallax. See post below for details.
[attachment deleted by admin]
It scrolls into/out of view at a specified speed.
Points that scroll off the screen (to the left) are removed from the array.
When a new hill is needed, it creates one (toggling the direction up or down).
It uses this new hill (really just a height value) and slices up the hill horizontally into segments.
It will scroll this new hill into view, then repeat endlessly.
In memory, the array should only have enough segment slices to fill the screen, plus a bit before, and the next hill yet to be shown.
Controls:
[<-] - Cursor Left - Decrease Speed (by 0.05)
[->] - Cursor Right - Increase Speed (by 0.05)
[1] - Change display mode to 'MODE_TEXTURE'
[2] - Change display mode to 'MODE_TEXTURE_WIRE'
[3] - Change display mode to 'MODE_WIRE'
[4] - Change display mode to 'MODE_POLYGON'
[5] - Change display mode to 'MODE_SOLID'
(The display modes are explained in the source code, nothing much really, just for debugging)
I'm not a graphics guy, so I just whipped up a sample texture to demo this.
If you notice the light brown bottom dirt border, and if you switch to 'MODE_TEXTURE_WIRE', the middle line follows this dirt border.
(This is configurable in code by a percentage - it is set to 25%)
While this may not affect the display too much, you can use the mid-line coordinates to wire up a physics engine and use that line for the colliders.
(It does affect the display a bit, kinda confusing, check out the code where ever I used that variable).
I tried commenting the code, but I ran out of time. May update it later.
I attached the png texture I used.
And I zipped up the project too.
I hope it works, I'm in a rush!
Notes:
- a 'hill' is just the x/y coordinates of a peak or valley
- segments are how I slice up the hill and use a COSINE function to provide the rolling effect
- when the texture runs out, I just start it over again, but it may not have fully displayed the full texture, so when it joins with the next segment, it may not join up nicely - not sure how to get around this
- play with the hill settings, but if it is too much of a slope, the polygons will overlap, I could provide a work around, but it is simpler to just live with this constraint
RollingHills.gbas
Code (glbasic) Select
GLOBAL sx%, sy%
GLOBAL speed# = 4.0
GLOBAL draw_mode% = MODE_TEXTURE
GLOBAL sp_texture%
GLOBAL hills AS THillPoints
GLOBAL hills2 AS THillPoints
Main()
END
FUNCTION Main:
SYSTEMPOINTER TRUE
GETSCREENSIZE sx, sy
SEEDRND MID$(PLATFORMINFO$("time"), 17, 2)
LIMITFPS 60
IF NOT SETCURRENTDIR("Media")
DEBUG "Media Folder Not Found . . . Exiting!\n"
END
ENDIF
// Initialize 'hill' TYPE
sp_texture = GENSPRITE()
LOADSPRITE "grass.png", sp_texture // Load texture
hills.sp_texture = sp_texture
hills.x_width = 15 // How wide each section strip is, the wider, the more 'chunky' looking hills
GETSPRITESIZE sp_texture, hills.texture_width, hills.texture_height // Sprite size of the ground texture
hills.texture_ht_up = hills.texture_height * 0.75 // How tall to make the top portion
hills.texture_ht_dn = hills.texture_height * 0.25 // How tall to make the bottom portion (together should add to 1.0)
hills.x_change_min = 180 // When creating a new hill peak, it must be at LEAST this distance from previous
hills.x_change_max = 220 // When creating a new hill peak, it must be at MOST this distance from previous
hills.y_change_min = 50 // When creating a new hill peak, the peak must be at LEAST this much higher/lower
hills.y_change_max = 190 // When creating a new hill peak, the peak must be at MOST this much higher/lower
hills.y_min = sy * 0.40 // Keep hills below top 20% of screen
hills.y_max = sy * 0.95 // Keep hills above bottom 5% of screen
hills.rgb_bottom = RGB(100,60,20)
GenerateHills(hills) // Initial seeding of hills to fill screen at start
// Initialize 'hill2' TYPE
sp_texture = GENSPRITE()
LOADSPRITE "snow.png", sp_texture // Load texture
hills2.sp_texture = sp_texture
hills2.x_width = 25 // How wide each section strip is, the wider, the more 'chunky' looking hills
GETSPRITESIZE sp_texture, hills2.texture_width, hills2.texture_height // Sprite size of the ground texture
hills2.texture_ht_up = hills2.texture_height * 0.25 // How tall to make the top portion
hills2.texture_ht_dn = hills2.texture_height * 0.75 // How tall to make the bottom portion (together should add to 1.0)
hills2.x_change_min = 220 // When creating a new hill peak, it must be at LEAST this distance from previous
hills2.x_change_max = 280 // When creating a new hill peak, it must be at MOST this distance from previous
hills2.y_change_min = 70 // When creating a new hill peak, the peak must be at LEAST this much higher/lower
hills2.y_change_max = 220 // When creating a new hill peak, the peak must be at MOST this much higher/lower
hills2.y_min = sy * 0.15 // Keep hills below top 20% of screen
hills2.y_max = sy * 0.60 // Keep hills above bottom 5% of screen
hills2.rgb_bottom = RGB(30,45,60)
GenerateHills(hills2) // Initial seeding of hills to fill screen at start
// Main Loop
WHILE TRUE
// Speed: "<-" and "->" cursor to decrease / increase
IF KEY(203)
DEC speed, 0.05
IF speed < 0.1 THEN speed = 0.1
ELSEIF KEY(205)
INC speed, 0.05
IF speed > 20 THEN speed = 20
ENDIF
// Draw Mode: Good for see what is happening behind the scenes
LOCAL key_hit$
key_hit$ = INKEY$()
SELECT key_hit$
CASE "1"
draw_mode = MODE_TEXTURE
CASE "2"
draw_mode = MODE_TEXTURE_WIRE
CASE "3"
draw_mode = MODE_WIRE
CASE "4"
draw_mode = MODE_POLYGON
CASE "5"
draw_mode = MODE_SOLID
ENDSELECT
CLEARSCREEN RGB(0,100,220) // Blue background - azure
ALPHAMODE -1.0 // In case your texture has alpha values
SMOOTHSHADING FALSE // Don't pixelate the graphics
hills2.Draw(draw_mode) // Draw current hills using current draw mode
hills2.Scroll(speed*0.3) // Scroll the hills left based on speed - if needed, a new hill will be generated
hills.Draw(draw_mode) // Draw current hills using current draw mode
hills.Scroll(speed) // Scroll the hills left based on speed - if needed, a new hill will be generated
SHOWSCREEN
WEND
ENDFUNCTION
FUNCTION GenerateHills: hill AS THillPoints
hill.Clear()
WHILE hill.hill_next.x < (sx + hill.x_change_max)
hill.GenerateNewHill()
WEND
ENDFUNCTION
types.gbas
Code (glbasic) Select
CONSTANT MODE_TEXTURE% = 1 // Normal textured display
CONSTANT MODE_TEXTURE_WIRE% = 2 // Wire outline overlayed on textured display
CONSTANT MODE_WIRE% = 3 // Wire outine of all calculated vectors
CONSTANT MODE_SOLID% = 4 // Fills in quads will solid colour - kinda neat when fast!
CONSTANT MODE_POLYGON% = 5 // Same as 'SOLID', except shades each polygon to see each polygon
CONSTANT PI = 3.14159
// General vector routine, makes other code cleaner
TYPE TVector
x#
y#
FUNCTION Set: x#, y#
self.x = x
self.y = y
ENDFUNCTION
FUNCTION Copy: v AS TVector
self.x = v.x
self.y = v.y
ENDFUNCTION
FUNCTION Scale: amount#
self.x = self.x * amount
self.y = self.y * amount
ENDFUNCTION
FUNCTION AverageWith AS TVector: v AS TVector
LOCAL rv AS TVector
rv.x = (self.x + v.x) / 2.0
rv.y = (self.y + v.y) / 2.0
RETURN rv
ENDFUNCTION
// Calc the magnitude / length of a vector
FUNCTION Magnitude#:
RETURN SQR((self.x * self.x) + (self.y * self.y))
ENDFUNCTION
ENDTYPE
// A hill point is a point along the curved hill, a new one is generated every 'self.x_width' pixels
TYPE THillPoint
pu AS TVector // Higher point - used for top portion of texture (grass)
pm AS TVector // Base/mid point - good to use for physics engine as the 'contact' location
pd AS TVector // Lower point - used for the bottom portion of texture (dirt border)
texture_x# // How far along texture this point is
ENDTYPE
TYPE THillPoints
points[] AS THillPoint // All of the points along all of the hills
x_width% // How far between curve segments (the more, the smoother the curve)
texture_height# // How tall the ground texture is
texture_ht_up# // How tall the ground texture is - above the base point
texture_ht_dn# // How tall the ground texture is - below the base point
texture_width# // How wide the ground texture is
y_min% // How high up the screen a hill peak can reach
y_max% // How low on the screen a hill valley can reach
x_change_min% // New hill peaks/valleys must be at least this 'x' distance from previous
x_change_max% // New hill peaks/valleys must be at most this 'x' distance from previous
y_change_min% // New hills must be at least this far above or below
y_change_max% // New hills must be at most this far above or below
peak_valley_toggle% // Remember if previous hill was a peak or a valley, toggle each time
sp_texture% // Sprite ID of hill top texture
rgb_bottom% // Colour below the curve and texture (the bottom dirt colour)
hill_prev AS TVector // Used when calculating new hills
hill_next AS TVector // The current hill vector will be stored here
// Used to clear / initialize the data
FUNCTION Clear:
LOCAL y_adjust% = 30 // Used to make sure we don't start too high or too low
DIM self.points[0] // Clear previous points
self.hill_next.Set(0, RND(self.y_max - self.y_min - (y_adjust * 2)) + self.y_min + y_adjust) // Initialize 'hill_next' at a random valid 'y' pos
self.hill_prev.Set(0, 0) // Initialize 'hill_prev'
ENDFUNCTION
// Draw current hill points - various draw modes are available
FUNCTION Draw: mode% = MODE_TEXTURE
LOCAL hx%
LOCAL xy[] AS TVector
LOCAL uv[] AS TVector
SELECT mode
CASE MODE_WIRE
FOR hx = 1 TO LEN(self.points[]) - 1
DRAWLINE self.points[hx-1].pu.x, self.points[hx-1].pu.y, self.points[hx].pu.x, self.points[hx].pu.y, RGB(255,255,255)
DRAWLINE self.points[hx-1].pm.x, self.points[hx-1].pm.y, self.points[hx].pm.x, self.points[hx].pm.y, RGB(255,255,255)
DRAWLINE self.points[hx-1].pd.x, self.points[hx-1].pd.y, self.points[hx].pd.x, self.points[hx].pd.y, RGB(255,255,255)
DRAWLINE self.points[hx].pu.x, self.points[hx].pu.y, self.points[hx].pd.x, self.points[hx].pd.y, RGB(255,255,255)
NEXT
CASE MODE_POLYGON
STARTPOLY -1, 2
FOR hx = 1 TO LEN(self.points[]) - 1
PolyQuick(self.points[hx-1].pu, self.points[hx].pu, self.points[hx-1].pd, self.points[hx].pd, RGB(250,175,0), RGB(175,120,0))
NEXT
ENDPOLY
CASE MODE_SOLID
STARTPOLY -1, 2
FOR hx = 1 TO LEN(self.points[]) - 1
PolyQuick(self.points[hx-1].pu, self.points[hx].pu, self.points[hx-1].pd, self.points[hx].pd, RGB(250,180,0), RGB(250,180,0))
NEXT
ENDPOLY
CASE MODE_TEXTURE
DIM xy[2]
DIM uv[4]
xy[0].y = sy
xy[1].y = sy
// Bottom area first (dirt?)
STARTPOLY -1, 2
FOR hx = 1 TO LEN(self.points[]) - 1
IF self.points[hx].pm.x < sx + (self.x_width*2)
xy[0].x = self.points[hx-1].pd.x
xy[1].x = self.points[hx].pd.x
PolyQuick(self.points[hx-1].pm, self.points[hx].pm, xy[0], xy[1], self.rgb_bottom, self.rgb_bottom)
ENDIF
NEXT
ENDPOLY
// Now draw hills
uv[0].y = 0
uv[1].y = 0
uv[2].y = self.texture_height
uv[3].y = self.texture_height
STARTPOLY self.sp_texture, 2
FOR hx = 1 TO LEN(self.points[]) - 1
IF self.points[hx].pm.x < sx + (self.x_width*2)
uv[0].x = self.points[hx-1].texture_x
uv[1].x = self.points[hx].texture_x
IF uv[0].x = self.texture_width THEN uv[0].x = 0
IF uv[1].x < uv[0].x
uv[1].x = uv[0].x + self.x_width
ENDIF
uv[2].x = uv[0].x
uv[3].x = uv[1].x
PolyDraw(self.points[hx-1].pu, self.points[hx].pu, self.points[hx-1].pd, self.points[hx].pd, uv[0], uv[1], uv[2], uv[3])
ENDIF
NEXT
ENDPOLY
CASE MODE_TEXTURE_WIRE
self.Draw(MODE_TEXTURE) // First draw in texture mode
self.Draw(MODE_WIRE) // Then overlay with wire mode
ENDSELECT
ENDFUNCTION
FUNCTION Scroll: x#
FOREACH p IN self.points[]
DEC p.pu.x, x#
DEC p.pm.x, x#
DEC p.pd.x, x#
IF (p.pu.x < -(self.x_width*2)) AND (p.pd.x < -(self.x_width*2)) THEN DELETE p // Delete point if too far to the left offscreen
NEXT
DEC self.hill_prev.x, x#
DEC self.hill_next.x, x#
// Generate new hill yet?
IF self.hill_next.x < (sx + (self.x_width * 2)) THEN self.GenerateNewHill()
ENDFUNCTION
FUNCTION GenerateNewHill:
LOCAL x_dist%
ALIAS h0 AS self.hill_prev
ALIAS h1 AS self.hill_next
h0.Copy(h1) // Move old 'next' to 'prev'
// Random 'x' distance from previous peak
x_dist = self.x_change_min + RND(self.x_change_max - self.x_change_min)
x_dist = (x_dist / self.x_width) * self.x_width // Snap to 'hillPoints.x_width'
h1.x = h0.x + x_dist
// Random 'y' distance from previous peak
IF self.peak_valley_toggle = TRUE
// New point will be below previous (ie: a 'valley')
h1.y = h0.y + (self.y_change_min + RND(self.y_change_max - self.y_change_min))
ELSE
// New point will be above previous (ie: a 'peak')
h1.y = h0.y - (self.y_change_min + RND(self.y_change_max - self.y_change_min))
ENDIF
// Adjust 'y' to keep in range
IF h1.y < (self.y_min + self.texture_ht_up) THEN h1.y = (self.y_min + self.texture_ht_up)
IF h1.y > (self.y_max - self.texture_ht_dn) THEN h1.y = (self.y_max - self.texture_ht_dn)
// Toggle peak / valley bit for next entry
self.peak_valley_toggle = NOT self.peak_valley_toggle
self.FillCurve()
ENDFUNCTION
// Fill in curve between these two 'hillPeaks' entries
FUNCTION FillCurve:
LOCAL segments#, sx#
LOCAL dx#, da#, ymid#, ampl#
LOCAL hillPoint AS THillPoint
LOCAL v_delta AS TVector
LOCAL partial#, overage#
LOCAL length#
ALIAS h0 AS self.hill_prev
ALIAS h1 AS self.hill_next
segments = (h1.x - h0.x) / self.x_width
dx = (h1.x - h0.x) / segments
da = PI / segments
ymid = (h0.y + h1.y) / 2.0
ampl = (h0.y - h1.y) / 2.0
FOR sx = 0 TO segments-1
hillPoint.pm.x = h0.x + sx*dx
hillPoint.pm.y = ymid + ampl * COS(sx*da * 180.0/PI)
IF LEN(self.points[]) = 0
hillPoint.texture_x = 0
ELSE
v_delta.Set(hillPoint.pm.x - self.points[-1].pm.x, hillPoint.pm.y - self.points[-1].pm.y)
length = v_delta.Magnitude()
hillPoint.texture_x = self.points[-1].texture_x + length
// Are we at the end of the texture, time to start at the beginning again
IF hillPoint.texture_x > self.texture_width
overage = hillPoint.texture_x - self.texture_width // How far over the end of the texture are we?
partial = overage / length // As a percentage (between 0 and 1)
// If it's really small, just stretch it to tne end of the texture
IF partial < 0.1
hillPoint.texture_x = self.texture_width
// If it's really large, snap the previous point to the end of the texture, and start this entry over
ELSEIF partial > 0.9
hillPoint.texture_x = length
self.points[-1].texture_x = self.texture_width
// Somewhere in between? Then create a mid-point entry by splitting this polygon in two
ELSE
// Mid point entry
hillPoint.texture_x = self.texture_width
hillPoint.pm.x = h0.x + (sx-partial)*dx
hillPoint.pm.y = ymid + ampl * COS((sx-partial)*da * 180.0/PI)
self.AddPoint(hillPoint)
// Regular entry
hillPoint.texture_x = overage
hillPoint.pm.x = h0.x + sx*dx
hillPoint.pm.y = ymid + ampl * COS(sx*da * 180.0/PI)
ENDIF
ENDIF
ENDIF
self.AddPoint(hillPoint)
NEXT
ENDFUNCTION
FUNCTION AddPoint: h AS THillPoint
DIMPUSH self.points[], h
IF LEN(self.points[]) = 1
self.points[0].pu.Set(0, self.points[0].pm.y - self.texture_ht_up)
self.points[0].pd.Set(0, self.points[0].pm.y + self.texture_ht_dn)
RETURN
ENDIF
ALIAS h0 AS self.points[-2]
ALIAS h1 AS self.points[-1]
LOCAL angle#
LOCAL v1 AS TVector
LOCAL v0 AS TVector
// Calculate 'p2' of HillPoint - form rectangle with previous point
angle = ATAN(h1.pm.y - h0.pm.y, h1.pm.x - h0.pm.x) -90// Angle of the two points
v1.x = COS(angle) * self.texture_ht_up + h1.pm.x
v1.y = SIN(angle) * self.texture_ht_up + h1.pm.y
v0.x = COS(angle) * self.texture_ht_up + h0.pm.x
v0.y = SIN(angle) * self.texture_ht_up + h0.pm.y
h0.pu = h0.pu.AverageWith(v0)
h1.pu.Copy(v1)
v1.x = COS(angle) * -self.texture_ht_dn + h1.pm.x
v1.y = SIN(angle) * -self.texture_ht_dn + h1.pm.y
v0.x = COS(angle) * -self.texture_ht_dn + h0.pm.x
v0.y = SIN(angle) * -self.texture_ht_dn + h0.pm.y
h0.pd = h0.pd.AverageWith(v0)
h1.pd.Copy(v1)
ENDFUNCTION
ENDTYPE
FUNCTION PolyQuick: v1 AS TVector, v2 AS TVector, v3 AS TVector, v4 AS TVector, rgb1%, rgb2%
POLYNEWSTRIP
POLYVECTOR v1.x, v1.y, 0, 0, rgb1 // TL
POLYVECTOR v3.x, v3.y, 0, 1, rgb1 // BL
POLYVECTOR v2.x, v2.y, 1, 0, rgb2 // TR
POLYVECTOR v4.x, v4.y, 1, 1, rgb2 // BR
ENDFUNCTION
FUNCTION PolyDraw: v1 AS TVector, v2 AS TVector, v3 AS TVector, v4 AS TVector, uv1 AS TVector, uv2 AS TVector, uv3 AS TVector, uv4 AS TVector
LOCAL rgb1% = RGB(255,255,255)
POLYNEWSTRIP
POLYVECTOR v1.x, v1.y, uv1.x, uv1.y, rgb1 // TL
POLYVECTOR v3.x, v3.y, uv3.x, uv3.y, rgb1 // BL
POLYVECTOR v2.x, v2.y, uv2.x, uv2.y, rgb1 // TR
POLYVECTOR v4.x, v4.y, uv4.x, uv4.y, rgb1 // BR
ENDFUNCTION
[EDIT] Fixed most / all bugs. Updated demo for parallax. See post below for details.
[attachment deleted by admin]