|
Forum Index : Microcontroller and PC projects : CMM2: Outrun game
| Author | Message | ||||
| Martin H. Guru Joined: 04/06/2022 Location: GermanyPosts: 1479 |
Hello Leo, On Sunday, I took a look at Jake Gordon’s explanation and recreated it in MM-Basic (with help from Gemini). I haven't done any speed tests on the Pico yet but it runs quite smoothly on MMB4W and should work just as well on the CMM2. ![]() This could form the framework for the game. ' ========================================================================= ' 2.5D Testloop Martin H. 2026 ' MM-Basic Version based on the description by Jake Gordon ' https://jakesgordon.com/writing/javascript-racer-v3-hills/ ' ========================================================================= Option Explicit Option Default Integer mode 7 ' Bildschirm-Modus (z.B. 640x480) Const SCREEN_WIDTH = mm.hres Const SCREEN_HEIGHT = mm.vres ' ' Konstanten für die Straße und Kamera Const ROAD_WIDTH = 2000 Const SEGMENT_LENGTH = 200 Const DRAW_DISTANCE = 100 ' Globale Variablen mit DIM deklarieren Dim Float fieldOfView = 100 Dim Float cameraHeight = 1500 Dim Float cameraDepth = 1 / Tan((fieldOfView / 2) * Pi / 180) Dim Float playerZ = cameraHeight * cameraDepth Dim Float resolution = SCREEN_HEIGHT / 480 Dim Float position = 0 Dim Float playerX = 0 Dim Float speed = 75 ' Input values for the projection Dim Float prj_wy, prj_wz Dim Float prj_px, prj_py Dim Float prj_camz, prj_xoff Dim prj_looped ' Ausgabewerte der Projektion Dim prj_sx, prj_sy, prj_sw ' Temporary auxiliary variables for the calculation (declared globally) Dim Float prj_tx, prj_ty, prj_tz, prj_scale ' Two separate arrays of integers for the X and Y coordinates of the four corners Dim xpoly%(3) Dim ypoly%(3) ' ========================================================================= ' The road network (global arrays) ' ========================================================================= Const MAX_SEGMENTS = 3000 Dim integer seg_count = 0 Page write 1 ' The parallel arrays hold the route data Dim Float seg_world_y(MAX_SEGMENTS) Dim Float seg_world_z(MAX_SEGMENTS) Dim Float seg_curve(MAX_SEGMENTS) Dim seg_color(MAX_SEGMENTS) ' Subroutine to be added: Variables in the header are automatically local, ' no internal variables are redimensioned. Sub Add_Segment(curve As Float, y As Float) If seg_count >= (MAX_SEGMENTS-100) Then Exit Sub seg_world_y(seg_count) = y seg_world_z(seg_count) = seg_count * SEGMENT_LENGTH seg_curve(seg_count) = curve If Int(seg_count / 3) MOD 2 Then seg_color(seg_count) = 1 Else seg_color(seg_count) = 0 EndIf inc seg_count End Sub Function EaseInOut(a As Float, b As Float, pct As Float) As Float EaseInOut = a + (b - a) * ((-Cos(pct * Pi) / 2) + 0.5) End Function 'Here we use LOCAL for the loop variable Sub Build_Track seg_count = 0 'Reset to restart' ' Parameter description for Add_Road: ' Inlet, Hold, Outlet, Curve strength (- for left, + for right), Hill height ' --- OUTRUN LEVEL 1: COCONUT BEACH --- ' Start & Erste Hügel Add_Road 100, 100, 100, 0, 8 Add_Road 20, 20, 20, 0, 8 Add_Road 10, 10, 100, -5, 8 Add_Road 20, 50, 20, 30, 7 ' First big left-hand bend & straight Add_Road 100, 100, 100, -1.5, 0 Add_Road 26, 26, 26, 0, 7 ' A right-hand bend that leads into a hill Add_Road 100, 100, 5, 1.5, 0 Add_Road 0, 100, 0, 1.5, 20 Add_Road 0, 50, 100, 1.5, -10 ' Hügelige Gerade Add_Road 40, 40, 40, 0, 20 Add_Road 30, 60, 0, 0, -15 ' Long left-hand bend, straight, sharper left-hand bend Add_Road 20, 170, 85, -1.5, 0 Add_Road 50, 50, 50, 0, 0 Add_Road 60, 60, 60, -3.0, 0 Add_Road 70, 70, 70, 0, 0 ' A combination of S-bends and hills Add_Road 20, 20, 20, 3.0, 0 Add_Road 25, 25, 25, 0, -10 Add_Road 20, 40, 0, -3.0, 10 Add_Road 0, 80, 0, -3.0, -5 Add_Road 0, 70, 0, -3.0, 15 ' Outward-sweeping left-hand bend & long straight Add_Road 0, 50, 20, -3.0, 0 Add_Road 100, 100, 100, 0, 0 Add_Road 30, 30, 30, -3.0, 0 Add_Road 30, 30, 30, 0, 0 Add_Road 30, 30, 30, 0, 0 'The bumpy finish before the fork in the track' Add_Road 20, 20, 20, -3.5, 0 Add_Road 10, 60, 10, 3.5, 20 Add_Road 10, 90, 10, -3.5, 25 Add_Road 10, 10, 10, 0, 0 Add_Road 50, 50, 50, 0, 0 Add_Road 10, 10, 10, 0, 5 Add_Road 10, 10, 10, 0, -5 Add_Road 10, 10, 10, 0, 5 Add_Downhill_To_End 150 End Sub Build_Track Dim Float trackLength = seg_count * SEGMENT_LENGTH Sub Add_Downhill_To_End num_segments Local startY, n Local Float hillPerSegment If seg_count = 0 Then Exit Sub ' Determine the current altitude at the end of the OutRun track startY = seg_world_y(seg_count - 1) ' Calculate how much we need to decrease/increase per segment to end up at 0 ' (Taper off as a negative slope) hillPerSegment = -startY / (num_segments * SEGMENT_LENGTH) ' Add the run-out straight, which gently takes us to the finish line ' We use a slight left-hand bend (-1.0) to visually signal the finish line Add_Road Int(num_segments/3), Int(num_segments/3), Int(num_segments/3), -1.0, hillPerSegment End Sub Sub Project ' 1. Transformieren prj_tx = (prj_px * ROAD_WIDTH) - prj_xoff prj_ty = prj_py + cameraHeight prj_tz = prj_wz - prj_camz ' 2. Runden-Loop prüfen If prj_looped Then inc prj_tz , trackLength EndIf ' 3. Division durch Null verhindern If prj_tz <= 0 Then prj_tz = 1 EndIf ' 4. Skalieren und Projizieren prj_scale = cameraDepth / prj_tz ' 5. Ergebnis in die Ausgangsvariablen schreiben prj_sx = (SCREEN_WIDTH / 2) + (prj_scale * prj_tx * SCREEN_WIDTH / 2) prj_sy = (SCREEN_HEIGHT / 2) - (prj_scale * (prj_wy - prj_ty) * SCREEN_HEIGHT / 2) prj_sw = prj_scale * ROAD_WIDTH * SCREEN_WIDTH / 2 End Sub ' ========================================================================= ' Render-Engine ' ========================================================================= Sub Render_Road Local baseSeg, s_idx, looped, n, maxy Local Float basePct, playerY, p1_y, p2_y Local Float r_x, r_dx ' Werte für das aktuelle Segment berechnen baseSeg = Int(position / SEGMENT_LENGTH) MOD seg_count basePct = (position MOD SEGMENT_LENGTH) / SEGMENT_LENGTH p1_y = seg_world_y(baseSeg) p2_y = seg_world_y((baseSeg + 1) MOD seg_count) playerY = p1_y + (p2_y - p1_y) * basePct maxy = SCREEN_HEIGHT r_x = 0 r_dx = - (seg_curve(baseSeg) * basePct) ' Lokale Speicher für die projizierten Punkte Local p1_sx, p1_sy, p1_sw Local p2_sx, p2_sy, p2_sw For n = 0 To DRAW_DISTANCE - 1 if n=Draw_DISTANCE - 1 then Box 0,0,SCREEN_WIDTH,p1_sy+1,,rgb(Cyan),rgb(Cyan) s_idx = (baseSeg + n) MOD seg_count looped = (s_idx < baseSeg) ' --- PROJEKTION PUNKT 1 (p1) --- ' Daten übergeben prj_wy = seg_world_y(s_idx) prj_wz = seg_world_z(s_idx) prj_px = playerX prj_py = playerY prj_camz = position prj_looped = looped prj_xoff = r_x Project ' Berechnen ' Ergebnisse sichern p1_sx = prj_sx : p1_sy = prj_sy : p1_sw = prj_sw ' --- PROJEKTION PUNKT 2 (p2) --- ' Daten übergeben (Nächstes Segment) prj_wy = seg_world_y((s_idx + 1) MOD seg_count) prj_wz = seg_world_z((s_idx + 1) MOD seg_count) prj_xoff = r_x + r_dx Project ' Berechnen ' Ergebnisse sichern p2_sx = prj_sx : p2_sy = prj_sy : p2_sw = prj_sw ' Kurven-Akkumulation r_x = r_x + r_dx r_dx = r_dx + seg_curve(s_idx) ' Clipping prüfen If p2_sy >= p1_sy Or p2_sy >= maxy Then Continue For EndIf ' Zeichnen aufrufen (Variablenübergabe via Globals oder kurze Liste) Draw_Segment_Trapezoid p1_sx, p1_sy, p1_sw, p2_sx, p2_sy, p2_sw, seg_color(s_idx) maxy = p1_sy Next n End Sub Sub Add_Road enter_seg, hold_seg, leave_seg, curve As Float, hillY As Float Local startY, endY, total, n Local Float c_val, y_val startY = 0 If seg_count > 0 Then startY = seg_world_y(seg_count - 1) EndIf ' hill-Y wird hier als direkte Neigung pro Segment interpretiert, ' das verhindert mathematische Rundungsfehler bei schnellen Wechseln total = enter_seg + hold_seg + leave_seg endY = startY + (hillY * total) ' curve ' 1. Ease In For n = 0 To enter_seg - 1 c_val = EaseInOut(0, curve, n / enter_seg) y_val = EaseInOut(startY, endY, n / total) Add_Segment c_val, y_val Next n ' 2. Hold For n = 0 To hold_seg - 1 c_val = curve y_val = EaseInOut(startY, endY, (enter_seg + n) / total) Add_Segment c_val, y_val Next n ' 3. Ease Out For n = 0 To leave_seg - 1 c_val = EaseInOut(curve, 0, n / leave_seg) y_val = EaseInOut(startY, endY, (enter_seg + hold_seg + n) / total) Add_Segment c_val, y_val Next n End Sub Sub Draw_Segment_Trapezoid x1, y1, w1, x2, y2, w2, colorType Local c_road, c_grass,n If colorType = 0 Then c_road = RGB(0, 0, 255) c_grass = RGB(0, 170, 0) Else c_road = RGB(0, 85,255) c_grass = RGB(0, 255, 0) EndIf if y2>SCREEN_HEIGHT then y2=SCREEN_HEIGHT ' Fill in X-coordinates (integer assignment) xpoly%(0) = Int(x1 - w1) xpoly%(1) = Int(x2 - w2) xpoly%(2) = Int(x2 + w2) xpoly%(3) = Int(x1 + w1) For n = 0 To 3 if xpoly%(n)> SCREEN_WIDTH then xpoly%(n)= SCREEN_WIDTH next ' Fill in Y-coordinates (integer assignment) ypoly%(0) = Int(y1) ypoly%(1) = Int(y2) ypoly%(2) = Int(y2) ypoly%(3) = Int(y1) 'Grass baseline box 0, y1, SCREEN_WIDTH, y2-y1,, c_grass,c_grass 'Road segment POLYGON 4, xpoly%(), ypoly%(), c_road, c_road End Sub ' ========================================================================= ' (Game Loop) ' ========================================================================= CLS 0 Dim k$ DO inc position,speed If position >= trackLength Then inc position, - trackLength ' Keypress? k$ = INKEY$ If k$ = "a" Or k$ = "A" Then playerX = playerX - 0.05 If k$ = "d" Or k$ = "D" Then playerX = playerX + 0.05 If k$ = chr$(27) then end 'start Render-Engine Render_Road TEXT 10, 10, "SPEED: " + Str$(speed) page copy 1,0 ' PAUSE 30 LOOP Cheers Martin Edited 2026-05-19 20:14 by Martin H. 'no comment |
||||
| Martin H. Guru Joined: 04/06/2022 Location: GermanyPosts: 1479 |
Background graphics for Outrun all 15 stages Hello, in case anyone’s interested: I’ve revamped the background graphics for all 15 levels of the game. To do this, I ripped the graphics from the Sega Mega Drive/Genesis version. These are originally 640 pixels wide. To save on resources, I’ve compressed them horizontally (C64-style, It doesn’t look as bad as it sounds, and it’s just the background on the horizon, where you can’t make out much anyway )PNG for CMM2 Windows, BMP for Pico RGB 121 ![]() MegaBG.zip I’ve also included a small sample programme so you can take a look at the backgrounds on Windows or on the Pico. 'Test / Demo for Outrun backgrounds Pico=1 If Pico Then MODE 2 FRAMEBUFFER create f Else MODE 7 End If CLS If Pico Then FRAMEBUFFER write f :Else :page write 1:EndIf Dim FN$(14)LENGTH 2 =("01","2A","2B","3A","3B","3C","4A","4B","4C","4D","5A","5B","5C","5D","5E") grass%=RGB(0,85,0) Dim Integer s=1 'Scrollspeed 1-8 Do For N=0 To 14 If Pico Then FRAMEBUFFER write f :Else :page write 1:EndIf LN$=FN$(N) If Pico Then Load image LN$+".BMP" 'Save compressed image LN$+".BMP",0,0,312,80 Else Load png LN$+".png" End If sky%=Pixel(0,0) If Pico Then FRAMEBUFFER write n :Else :page write 0:EndIf Box 0,0,320,80,,Sky%,sky% Box 0,80,320,160,,grass%,grass% If Pico Then FRAMEBUFFER write f :Else :page write 1:EndIf For f=0 To 320 'Scroll Left Blit 0,0,312,0,s,80:Blit s,0,0,0,312,80 If Pico Then Blit Resize f,n,0,0,160,80,0,80,320,80 Else page write 0 image resize 0,0,160,80,0,80,320,80,1 page write 1 End If Pause 20 Next For f=0 To 320 'Scroll Right Blit 0,0,s,0,312,80:Blit 312,0,0,0,s,80 If Pico Then Blit Resize f,n,0,0,160,80,0,80,320,80 Else page write 0 image resize 0,0,160,80,0,80,320,80,1 page write 1 End If Pause 20 Next Next Loop Perhaps someone will find the time to give it a go. Edited 2026-07-04 01:42 by Martin H. 'no comment |
||||
| LeoNicolas Guru Joined: 07/10/2020 Location: CanadaPosts: 590 |
Thank you, Martin. I haven't given up on developing the game. I just needed to take a break due to some personal issues. I hope to restart development over the next few months. |
||||
| Martin H. Guru Joined: 04/06/2022 Location: GermanyPosts: 1479 |
Hi Leo, Good to hear from you. I summarized all of this today and combined the two approaches. This is now Picomite 2 running at 378 MHz, but unfortunately it’s only managing 18–22 fps so far.Video I still need to find a less resource-intensive approach for drawing the roads; it worked much faster in my other attempt, after all. Cheers Martin 'no comment |
||||
| Martin H. Guru Joined: 04/06/2022 Location: GermanyPosts: 1479 |
Hi Leo and everyone, Here’s a quick update. Outrun.zip Over the last two days, I’ve been working on the programme behind the high score screen ![]() It’s more or less finished, but you can’t enter your details yet – after all, you haven’t achieved a score yet. In the game, I’m still trying to increase my speed by improving my driving technique. The tracks are already in the txt folder, but I haven’t achieved anything worth showing off yet. It’s too hot to think straight at the moment, anyway.That’s why I’ve spent more of my free time tweaking pixels and music than programming. ![]() It’s running well, but unfortunately still only at about 22 fps at 378 kHz (I’m working on it, see above). The music tracks are included as MOD files. Except for “Last Wave”, the jazzy high-score track – I’ve kept that as a FLAC file for the sake of simplicity. Sound effects haven’t been integrated yet. Just giving it a go. Cheers Martin Edited 2026-07-15 22:32 by Martin H. 'no comment |
||||
| Volhout Guru Joined: 05/03/2018 Location: NetherlandsPosts: 5991 |
Hi Martin, Tried this on RP2040VGA V60300. I get "out of heap memory" in line 72. Tried to limit max segments to 1000 (from 3500) but get the same error message. Are you running a 2350 ? Or an older MMBasic (6.02.01 ? ). Regards, Volhout PicomiteVGA PETSCII ROBOTS |
||||
| PilotPirx Senior Member Joined: 03/11/2020 Location: GermanyPosts: 126 |
It looks damn good—I can't wait to try it out. ![]() Hopefully there will be a CMM2 version later on, too!! Edited 2026-07-15 23:21 by PilotPirx |
||||
| Martin H. Guru Joined: 04/06/2022 Location: GermanyPosts: 1479 |
Hi Harm, Thank you for testing. Yes, you’re right, I’m currently working on a 2350 (HDMI), but the aim is to move on to the Pico 2040 VGA as well. At the moment, the route data array takes up a lot of space, but that will change with the new method; I plan to load only the part of the route data that’s currently needed – essentially streaming the route. It is, of course, in the nature of things that the file is only ever read in one direction; this makes it possible to keep the file open in read-only mode and only read the necessary (future) data. Please have a go at testing whether the ".Highscore.Bas" file works for you. Cheers Martin 'no comment |
||||
| Volhout Guru Joined: 05/03/2018 Location: NetherlandsPosts: 5991 |
Hi Martin, Yes Highscore.bas works (after I corrected a spurious character in line 100). With the cursor keys you can highlight characters, there is a countdown clock running right top. But I could not really edit something. Regards, Volhout PicomiteVGA PETSCII ROBOTS |
||||
| Martin H. Guru Joined: 04/06/2022 Location: GermanyPosts: 1479 |
Leo is working on the CMM2 version, but I think it would help if he could take over some of the components I’ve already programmed. My plan is as follows: I broke down a video of all the arcade levels into individual frames and analyzed them frame by frame to see exactly what happens and when. The game starts with an intro animation that is simply played and can then be purged from memory. At the start, the player is driving slowly; a drop in the frame rate wouldn’t be noticeable here, which gives the system a great opportunity to run garbage collection and load the next objects in the background. At this stage, only the absolutely necessary sprites are kept in memory. Elements like spectators or the start banner are no longer needed later on, so that memory is freed up again. Once the player reaches the first hill, the next set of sprites—like waves and surfers—can be loaded, and so on. In principle, new sprites are constantly replacing old ones, meaning all the "decorations" can be streamed. Forked paths always occur on flat ground, so there is no need to calculate the height separately at that moment. There are also no roadside decorations at any of these junctions. Once the player has chosen a direction, this gives the game enough time to load the new background first, followed by the sprites for the new level (bit by bit, so as not to cause visible lag). The next piece of scenery is the "Checkpoint" sign—and here, too, there are no other decorations along the road. Since those only appear after a few frames, I suspect they are loaded into sprite memory at that exact moment. From there, the actual level really gets going. To sum up: whenever the scene looks calm to the player, the hardware (Pico/CMM2) is actually working flat out in the background. Edited 2026-07-16 03:22 by Martin H. 'no comment |
||||
| Martin H. Guru Joined: 04/06/2022 Location: GermanyPosts: 1479 |
Hi Harm, After the INC command, that often happens on my Pico – for some inexplicable reason, ‘ghost characters’ suddenly appear.I don’t know if it’s down to the SD card. I did write that you can’t edit anything yet; that’s the GUI first of all. ![]() But for my taste, it’s pretty close to the original. I’m going to create a pseudo-score to implement the input and sorting. The little animation where the little cars drag the text across the screen is also missing. To make up for this, I’ve animated the headings (colour fade) – it just seemed like the right thing to do, even though it isn’t in the original. I handle the fade-in and fade-out in the same way as with Petscii Robots, but there’s a reason for that too. Once the high score is displayed, or rather once you get to the point of entering the high score, the game is over; after entering it, the main programme can be restarted. By using the fade-in and fade-out effect, I conceal the fact that it’s a different programme. For the Pico, this means it has a fresh system for every new round. Have a lovely evening Cheers Martin.. PS: Is it just as warm in the Netherlands at the moment? 'no comment |
||||
| Volhout Guru Joined: 05/03/2018 Location: NetherlandsPosts: 5991 |
Yes, too warm to do difficult tasks. Most of pico work happens in the evenings, when it cools down, sitting in our garden... Under the tree. Really looking forward to this game Martin. It sounds like there is a lot sequencing needed to balance the CPU load. Are you planning on using a state machine to do this (since in essence each level has a fixed start point, and a fixed end point, although you can select different roads to get there).?? Volhout Edited 2026-07-16 16:38 by Volhout PicomiteVGA PETSCII ROBOTS |
||||
| LeoNicolas Guru Joined: 07/10/2020 Location: CanadaPosts: 590 |
Hi Martin I'm traveling to visit family. I will be back home in August. I I will restart working on the project as soon as I'm back |
||||
| Martin H. Guru Joined: 04/06/2022 Location: GermanyPosts: 1479 |
I’m not sure what you mean by state machine. Technically speaking, all stages work in the same way. The only differences are the scenery, the route, the colour scheme and the background. I have the distribution of bends and elevation profiles in the CSV file. The individual stages simply have different start and end points. Here is a list of the start and end points for each stage in the CSV file. The list is not complete, but the line numbers are correct The 50, 150, etc. stages are the end points. They are simply reference points for the programme; they do not have specific line numbers. I found much of those data in a CPC Outrun version in QB, but the programme isn’t really transferable. However, the stage list can be expanded without any problems; each entry in the "route.csv" represents a section of the road valzes of. There you’ll find details on curves, elevation changes, decoration and so on. One only need at least 8 (preferably 12) of these in memory. At least, that’s my planThat’s nothing compared to the 3,000 Line field my test programme currently uses. And I can easily add, for example, instructions for reloading new graphics to ensure the timing is correct. Cheers Martin Edited 2026-07-17 01:56 by Martin H. 'no comment |
||||
| The Back Shed's forum code is written, and hosted, in Australia. | © JAQ Software 2026 |