Tech talk: How to create a node based mission screen

Tech talk: How to create a node based mission screen

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.

Come live chat with the developer and other gamers, get exclusive information on new games, features, discounts and BETA access! Join Discord: https://discord.gg/orangepixel