This post is just to show you the code I talked about in the video: How to program a NODE BASED mission select screen. Make sure to check the video to see what’s going on and what the whole idea behind this code is! I designed this code for the progress screen in Snake Core.
All this code is written in Java, but it doesn’t use a lot of java-specifics so should be easy to port to other languages.
So if you watched the video, let’s dive into the code:
First we have the mission node class, this is where we store the information about our node, but also screen-offsets:
public final static int maxStages=6; // maximum of stages/levels (stage-0 is top) public final static int maxNodesPerStage=5; // maximum possible nodes for every stage public class wgmission { public boolean inUse; // node connections map - we can connect to the 3 nodes below us, don't have to tho! public boolean[] nodeConnect; // nodes in the map have little offset, to make it look interesting public int missionNodeX; public int missionNodeY; // setup our mission type and variables public int missionType; public void init(int offsetX, int offsetY) { missionNodeX=offsetX; missionNodeY=offsetY; nodeConnect=new boolean[maxNodesPerStage]; for (int i=0;i<nodeConnect.length; i++) { nodeConnect[i]=false; } } public void setNode(int myType) { missionType=myType; inUse=true; } public void disable() { missionType=-1; inUse=false; } }
Now we initialise our mission-node array
// setup our array of mission nodes missions=new wgmission[maxStages][maxNodesPerStage]; // disable all nodes by default for (int i=0;i<maxStages; i++) { for (int j=0; j<maxNodesPerStage; j++) { missions[i][j]=new wgmission(); missions[i][j].disable(); // initialise, and give each node a "random" offset missions[i][j].init(getMyRandomValue(32)-16, getMyRandomValue(32)-16); // top-stage is our "end goals" so we give it a vertical offset to put it more above the rest if (i==0) missions[i][j].missionNodeY=-16; } }
and we start creating our random mission node:
// generate random paths from end-boss to bottom int maxpath=4; // amount of paths we create int nodeid; int currentnodeid; for (int pathid=maxpath; --pathid>=0;) { // start at the center node nodeid= (maxNodesPerStage>>1); // set our starting node at the bottom of the stages.. so we always have 1 starting node to begin the game missions[maxStages-1][nodeid].setNode( Globals.modeDefend ); // and by default connect all "second to last" nodes to that starting node at the bottom for (int i=0; i<maxNodesPerStage;i++) { missions[maxStages-2][i].nodeConnect[nodeid]=true; } // start generating paths from the top stage down to the second-to-last stage for (int stage=0; stage<maxStages-1; stage++) { currentnodeid=nodeid; // set this node's mission type missions[stage][nodeid].setNode( getMyRandomValue( Globals.modeMax ) ); if (stage==0) { // make sure to find unique nodes in the 2nd stage for every path we create from this starting node nodeid=(maxNodesPerStage>>1)-(maxpath>>1); while (missions[stage+1][nodeid].inUse) { nodeid++; if (nodeid>(maxNodesPerStage>>1)+(maxpath>>1)) nodeid=(maxNodesPerStage>>1)-(pathid>>1); } } else if (nodeid==0 || (stage==0 && pathid>2)) { // we're on left edge of the node map.. can only move straight down, or down+right nodeid += getMyRandomValue(2); } else if (nodeid==maxNodesPerStage-1 || (stage==0 && pathid<2)) { // on right edge of the node map.. can only move straight down, or down+left nodeid -= getMyRandomValue(2); } else { // pick random direction for our next node (so node-1, node+0, or node+1) nodeid += getMyRandomValue(3) - 1; } // connect current node to the new node we'll be making in stage below us missions[stage][currentnodeid].nodeConnect[nodeid]=true; } }
and finally we need to fix any cross-nodes (see video for explanation):
// fix cross-nodes! // some nodes might form a crossing X because they link to crossed nodes in the stage below.. let's fix those for (int stage=0; stage<maxStages-2; stage++) { for (int nid=0; nid<maxNodesPerStage-1; nid++) { if (missions[stage][nid].nodeConnect[nid+1] && missions[stage][nid+1].nodeConnect[nid]) { // we cross with the node next to us.. first thing to fix: make sure we both link to the nodes below us! missions[stage][nid].nodeConnect[nid]=true; missions[stage][nid+1].nodeConnect[nid+1]=true; // now decide to remove one or both of the cross nodes if (getMyRandomValue(100)<15) { // we're resolving both cross nodes - removing the links to the nodes below+beside us missions[stage][nid].nodeConnect[nid+1]=false; missions[stage][nid+1].nodeConnect[nid]=false; } else if (getMyRandomValue(100)<50) { // just remove our cross-node (not the one from our neighbor node) missions[stage][nid].nodeConnect[nid+1]=false; } else { // just remove the cross-link from the neighbor node to the one below us missions[stage][nid+1].nodeConnect[nid]=false; } // cross-nodes for this node are now solved! - easy! } } }
the last step is rendering it onto the screen, this is done using simple sprite drawing, and dots that make up the lines between nodes are also drawn using sprites.
// render our missions // setup variables to render lines int dotCount; int tx2; int ty2; int dx; int dy; int addx; int addy; // set the max size available for a single nodes int nodeWidth=48; int nodeHeight=48; boolean pathTaken; // our location on the screen - centered int tx=(Render.width>>1)-240; int ty=(Render.height>>1)-180; // start at top-node / top of screen ty+=16-missions[0][0].missionNodeY; for (int stage=0; stage<maxStages; stage++) { tx=(Render.width>>1)-((maxNodesPerStage-1)*(nodeWidth>>1)); for (int nodeid=0; nodeid<maxNodesPerStage; nodeid++) { if (missions[stage][nodeid].inUse) { // render connections to the 3 nodes below us dotCount=16; if (stage<maxStages-1) { for (int nodeconnect = 0; nodeconnect <=maxNodesPerStage; nodeconnect++) { if (nodeconnect >= 0 && nodeconnect = 0; ) { if (!pathTaken) { Render.dest.set((tx2 >> 4) + 8, (ty2 >> 4) + 8, (tx2 >> 4) + 8 + 2, (ty2 >> 4) + 8 + 2); Render.src.set(544, 32, 544 + 4, 32 + 4); Render.drawBitmap(myCanvas.sprites[0], false); } else { Render.dest.set((tx2 >> 4) + 8, (ty2 >> 4) + 8, (tx2 >> 4) + 8 + 2, (ty2 >> 4) + 8 + 2); Render.src.set(548, 32, 548 + 4, 32 + 4); Render.drawBitmap(myCanvas.sprites[0], false); } tx2 += addx; ty2 += addy; } } } } // render our node image Render.dest.set(tx+missions[stage][nodeid].missionNodeX,ty+missions[stage][nodeid].missionNodeY ,tx+missions[stage][nodeid].missionNodeX+16,ty+missions[stage][nodeid].missionNodeY+16); Render.src.set(560+(missions[stage][nodeid].missionType*16), 32, 560+16+(missions[stage][nodeid].missionType*16), 32+16); Render.drawBitmap(myCanvas.sprites[0],false); // if node isn't unlocked-yet, render a shaded version on top to make it look darker if (maxStageAvailable>stage) { Render.setAlpha(128); Render.dest.set(tx+missions[stage][nodeid].missionNodeX,ty+missions[stage][nodeid].missionNodeY ,tx+missions[stage][nodeid].missionNodeX+16,ty+missions[stage][nodeid].missionNodeY+16); Render.src.set(560+(missions[stage][nodeid].missionType*16), 48, 560+16+(missions[stage][nodeid].missionType*16), 48+16); Render.drawBitmap(myCanvas.sprites[0],false); Render.setAlpha(255); } // add selection arrow for the currently selected node if (nodeSelected==nodeid && stageSelected==stage) { tx2=tx+missions[stage][nodeid].missionNodeX; ty2=ty+missions[stage][nodeid].missionNodeY; Render.dest.set((tx2+8)-6, (ty2-14)+(arrowBounceY>>4),(tx2+8)+7,(ty2-14)+11+(arrowBounceY>>4)); Render.src.set(496,32, 496+13,32+11); Render.drawBitmap(myCanvas.sprites[0],false); if (arrowBounceYSpeed<16) arrowBounceYSpeed+=2; arrowBounceY+=arrowBounceYSpeed; if (arrowBounceY>=0) { arrowBounceY=0; arrowBounceYSpeed=-24; } } } tx+=nodeWidth; } ty+=nodeHeight; }
And that’s the code!
It’s a fairly simple solution for creating a node-based mission screen, but you can improve on this idea in various ways. I can imagine you could create a very big array of nodes and have the map expand in all directions making it a much more interesting looking map.
If you do anything interesting with the code, let me know! Love to see how it’s used and evolves.