• 18-19 College Green, Dublin 2
  • 01 685 9088
  • info@cunninghamwebsolutions.com
  • cunninghamwebsolutions
    Cunningham Web Solutions
    • Home
    • About Us
    • Our Services
      • Web Design
      • Digital Marketing
      • SEO Services
      • E-commerce Websites
      • Website Redevelopment
      • Social Media Services
    • Digital Marketing
      • Adwords
      • Social Media Services
      • Email Marketing
      • Display Advertising
      • Remarketing
    • Portfolio
    • FAQ’s
    • Blog
    • Contact Us
    MENU CLOSE back  

    How To Build An Endless Runner Game In Virtual Reality (Part 2)

    You are here:
    1. Home
    2. Web Design
    3. How To Build An Endless Runner Game In Virtual Reality (Part 2)
    Thumbnail for 22640
    Template trees moving towards player

    How To Build An Endless Runner Game In Virtual Reality (Part 2)

    How To Build An Endless Runner Game In Virtual Reality (Part 2)

    Alvin Wan

    2019-03-13T12:00:35+01:00
    2019-03-13T12:34:53+00:00

    In Part 1 of this series, we’ve seen how a virtual reality model with lighting and animation effects can be created. In this part, we will implement the game’s core logic and utilize more advanced A-Frame environment manipulations to build the “game” part of this application. By the end, you will have a functioning virtual reality game with a real challenge.

    This tutorial involves a number of steps, including (but not limited to) collision detection and more A-Frame concepts such as mixins.

    • Demo of the final product

    Prerequisites

    Just like in the previous tutorial, you will need the following:

    • Internet access (specifically to glitch.com);
    • A Glitch project completed from part 1. (You can continue from the finished product by navigating to https://glitch.com/edit/#!/ergo-1 and clicking “Remix to edit”;
    • A virtual reality headset (optional, recommended). (I use Google Cardboard, which is offered at $15 a piece.)

    Step 1: Designing The Obstacles

    In this step, you design the trees that we will use as obstacles. Then, you will add a simple animation that moves the trees towards the player, like the following:

    Template trees moving towards player (Large preview)

    These trees will serve as templates for obstacles you generate during the game. For the final part of this step, we will then remove these “template trees”.

    To start, add a number of different A-Frame mixins. Mixins are commonly-used sets of component properties. In our case, all of our trees will have the same color, height, width, depth etc. In other words, all your trees will look the same and therefore will use a few shared mixins.

    Note: In our tutorial, your only assets will be mixins. Visit the A-Frame Mixins page to learn more.

    In your editor, navigate to index.html. Right after your sky and before your lights, add a new A-Frame entity to hold your assets:

    <a-sky...></a-sky>
    
    <!-- Mixins -->
    <a-assets>
    </a-assets>
    
    <!-- Lights -->
    ...
    

    In your new a-assets entity, start by adding a mixin for your foliage. This mixins defines common properties for the foliage of the template tree. In short, it is a white, flat-shaded pyramid, for a low poly effect.

    <a-assets>
      <a-mixin id="foliage" geometry="
          primitive: cone;
          segments-height: 1;
          segments-radial:4;
          radius-bottom:0.3;"
         material="color:white;flat-shading: true;"></a-mixin>
    </a-assets>
    

    Just below your foliage mixin, add a mixin for the trunk. This trunk will be a small, white rectangular prism.

    <a-assets>
      ...
      <a-mixin id="trunk" geometry="
          primitive: box;
          height:0.5;
          width:0.1;
          depth:0.1;"
         material="color:white;"></a-mixin>
    </a-assets>
    

    Next, add the template tree objects that will use these mixins. Still in index.html, scroll down to the platforms section. Right before the player section, add a new tree section, with three empty tree entities:

    <a-entity id="tree-container" ...>
    
      <!-- Trees -->
      <a-entity id="template-tree-center"></a-entity>
      <a-entity id="template-tree-left"></a-entity>
      <a-entity id="template-tree-right"></a-entity>
    
      <!-- Player -->
      ...
    

    Next, reposition, rescale, and add shadows to the tree entities.

    <!-- Trees -->
    <a-entity id="template-tree-center" shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity>
    <a-entity id="template-tree-left" shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity>
    <a-entity id="template-tree-right" shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity>
    

    Now, populate the tree entities with a trunk and foliage, using the mixins we defined previously.

    <!-- Trees -->
    <a-entity id="template-tree-center" ...>
      <a-entity mixin="foliage"></a-entity>
      <a-entity mixin="trunk" position="0 -0.5 0"></a-entity>
    </a-entity>
    <a-entity id="template-tree-left" ...>
      <a-entity mixin="foliage"></a-entity>
      <a-entity mixin="trunk" position="0 -0.5 0"></a-entity>
    </a-entity>
    <a-entity id="template-tree-right" ...>
      <a-entity mixin="foliage"></a-entity>
      <a-entity mixin="trunk" position="0 -0.5 0"></a-entity>
    </a-entity>
    

    Navigate to your preview, and you should now see the following template trees.

    Template trees for obstacles

    Template trees for obstacles (Large preview)

    Now, animate the trees from a distant location on the platform towards the user. As before, use the a-animation tag:

    <!-- Trees -->
    <a-entity id="template-tree-center" ...>
      ...
      <a-animation attribute="position" ease="linear" from="0 0.6 -7" to="0 0.6 1.5" dur="5000"></a-animation>
    </a-entity>
    <a-entity id="template-tree-left" ...>
      ...
      <a-animation attribute="position" ease="linear" from="-0.5 0.55 -7" to="-0.5 0.55 1.5" dur="5000"></a-animation>
    </a-entity>
    <a-entity id="template-tree-right" ...>
      ...
      <a-animation attribute="position" ease="linear" from="0.5 0.55 -7" to="0.5 0.55 1.5" dur="5000"></a-animation>
    </a-entity>
    

    Ensure that your code matches the following.

    <a-entity id="tree-container"...>
    
    <!-- Trees -->
    <a-entity id="template-tree-center" shadow scale="0.3 0.3 0.3" position="0 0.6 0">
      <a-entity mixin="foliage"></a-entity>
      <a-entity mixin="trunk" position="0 -0.5 0"></a-entity>
      <a-animation attribute="position" ease="linear" from="0 0.6 -7" to="0 0.6 1.5" dur="5000"></a-animation>
    </a-entity>
    
    <a-entity id="template-tree-left" shadow scale="0.3 0.3 0.3" position="-0.5 0.55 0">
      <a-entity mixin="foliage"></a-entity>
      <a-entity mixin="trunk" position="0 -0.5 0"></a-entity>
      <a-animation attribute="position" ease="linear" from="-0.5 0.55 -7" to="-0.5 0.55 1.5" dur="5000"></a-animation>
    </a-entity>
    
    <a-entity id="template-tree-right" shadow scale="0.3 0.3 0.3" position="0.5 0.55 0">
      <a-entity mixin="foliage"></a-entity>
      <a-entity mixin="trunk" position="0 -0.5 0"></a-entity>
      <a-animation attribute="position" ease="linear" from="0.5 0.55 -7" to="0.5 0.55 1.5" dur="5000"></a-animation>
    </a-entity>
    
    <!-- Player -->
    ...
    

    Navigate to your preview, and you will now see the trees moving towards you.

    Template trees moving towards playerTemplate trees moving towards player (Large preview)

    Navigate back to your editor. This time, select assets/ergo.js. In the game section, setup trees after the window has loaded.

    /********
     * GAME *
     ********/
    
    ...
    
    window.onload = function() {
      setupTrees();
    }
    

    Underneath the controls but before the Game section, add a new TREES section. In this section, define a new setupTrees function.

    /************
     * CONTROLS *
     ************/
    
    ...
    
    /*********
     * TREES *
     *********/
    
    function setupTrees() {
    }
    
    /********
     * GAME *
     ********/
    
    ...
    

    In the new setupTrees function, obtain references to the template tree DOM objects, and make the references available globally.

    /*********
     * TREES *
     *********/
    
    var templateTreeLeft;
    var templateTreeCenter;
    var templateTreeRight;
    
    function setupTrees() {
      templateTreeLeft    = document.getElementById('template-tree-left');
      templateTreeCenter  = document.getElementById('template-tree-center');
      templateTreeRight   = document.getElementById('template-tree-right');
    }
    
    

    Next, define a new removeTree utility. With this utility, you can then remove the template trees from the scene. Underneath the setupTrees function, define your new utility.

    function setupTrees() {
        ...
    }
    
    function removeTree(tree) {
      tree.parentNode.removeChild(tree);
    }
    

    Back in setupTrees, use the new utility to remove the template trees.

    function setupTrees() {
      ...
    
      removeTree(templateTreeLeft);
      removeTree(templateTreeRight);
      removeTree(templateTreeCenter);
    }
    

    Ensure that your tree and game sections match the following:

    /*********
     * TREES *
     *********/
    
    var templateTreeLeft;
    var templateTreeCenter;
    var templateTreeRight;
    
    function setupTrees() {
      templateTreeLeft    = document.getElementById('template-tree-left');
      templateTreeCenter  = document.getElementById('template-tree-center');
      templateTreeRight   = document.getElementById('template-tree-right');
    
      removeTree(templateTreeLeft);
      removeTree(templateTreeRight);
      removeTree(templateTreeCenter);
    }
    
    function removeTree(tree) {
      tree.parentNode.removeChild(tree);
    }
    
    /********
     * GAME *
     ********/
    
    setupControls();  // TODO: AFRAME.registerComponent has to occur before window.onload?
    
    window.onload = function() {
      setupTrees();
    }
    

    Re-open your preview, and your trees should now be absent. The preview should match our game at the start of this tutorial.

    Part 1 finished product (Large preview)

    This concludes the template tree design.

    In this step, we covered and used A-Frame mixins, which allow us to simplify code by defining common properties. Furthermore, we leveraged A-Frame integration with the DOM to remove objects from the A-Frame VR scene.

    In the next step, we will spawn multiple obstacles and design a simple algorithm to distribute trees among different lanes.

    Step 2 : Spawning Obstacles

    In an endless runner game, our goal is to avoid obstacles flying towards us. In this particular implementation of the game, we use three lanes as is most common.

    Unlike most endless runner games, this game will only support movement left and right. This imposes a constraint on our algorithm for spawning obstacles: we can’t have three obstacles in all three lanes, at the same time, flying towards us. If that occurs, the player would have zero chance of survival. As a result, our spawning algorithm needs to accommodate this constraint.

    In this step, all of our code edits will be made in assets/ergo.js. The HTML file will remain the same. Navigate to the TREES section of assets/ergo.js.

    To start, we will add utilities to spawn trees. Every tree will need a unique ID, which we will naively define to be the number of trees that exist when the tree is spawned. Start by tracking the number of trees in a global variable.

    /*********
     * TREES *
     *********/
    
    ...
    var numberOfTrees = 0;
    
    function setupTrees() {
    ...
    

    Next, we will initialize a reference to the tree container DOM element, which our spawn function will add trees to. Still in the TREES section, add a global variable and then make the reference.

    ...
    var treeContainer;
    var numberOfTrees ...
    
    function setupTrees() {
        ...
        templateTreeRight   = ...
        treeContainer       = document.getElementById('tree-container');
        
        removeTree(...);
        ...
    }
    

    Using both the number of trees and the tree container, write a new function that spawns trees.

    function removeTree(tree) {
        ...
    }
    
    function addTree(el) {
      numberOfTrees += 1;
      el.id = 'tree-' + numberOfTrees;
      treeContainer.appendChild(el);
    }
    
    ...
    

    For ease-of-use later on, you will create a second function that adds the correct tree to the correct lane. To start, define a new templates array in the TREES section.

    var templates;
    var treeContainer;
    ...
    
    function setupTrees() {
        ...
        templates           = [templateTreeLeft, templateTreeCenter, templateTreeRight];
    
        removeTree(...);
        ...
    }
    

    Using this templates array, add a utility that spawns trees in a specific lane, given an ID representing left, middle, or right.

    function function addTree(el) {
        ...
    }
    
    function addTreeTo(position_index) {
      var template = templates[position_index];
      addTree(template.cloneNode(true));
    }
    

    Navigate to your preview, and open your developer console. In your developer console, invoke the global addTreeTo function.

    > addTreeTo(0);  # spawns tree in left lane
    

    Invoking addTreeTo manuallyInvoke addTreeTo manually (Large preview)

    Now, you will write an algorithm that spawns trees randomly:

  • Pick a lane randomly (that hasn’t been picked yet, for this timestep);
  • Spawn a tree with some probability;
  • If the maximum number of trees has been spawned for this timestep, stop. Otherwise, repeat step 1.
  • To effect this algorithm, we will instead shuffle the list of templates and process one at a time. Start by defining a new function, addTreesRandomly that accepts a number of different keyword arguments.

    function addTreeTo(position_index) {
        ...
    }
    
    /**
     * Add any number of trees across different lanes, randomly.
     **/
    function addTreesRandomly(
      {
        probTreeLeft = 0.5,
        probTreeCenter = 0.5,
        probTreeRight = 0.5,
        maxNumberTrees = 2
      } = {}) {
    }
    

    In your new addTreesRandomly function, define a list of template trees, and shuffle the list.

    function addTreesRandomly( ... ) {
      var trees = [
        {probability: probTreeLeft,   position_index: 0},
        {probability: probTreeCenter, position_index: 1},
        {probability: probTreeRight,  position_index: 2},
      ]
      shuffle(trees);
    }
    

    Scroll down to the bottom of the file, and create a new utilities section, along with a new shuffle utility. This utility will shuffle an array in place.

    /********
     * GAME *
     ********/
    
    ...
    
    /*************
     * UTILITIES *
     *************/
    
    /**
     * Shuffles array in place.
     * @param {Array} a items An array containing the items.
     */
    function shuffle(a) {
       var j, x, i;
       for (i = a.length - 1; i > 0; i--) {
           j = Math.floor(Math.random() * (i + 1));
           x = a[i];
           a[i] = a[j];
           a[j] = x;
       }
       return a;
    }
    

    Navigate back to the addTreesRandomly function in your Trees section. Add a new variable numberOfTreesAdded and iterate through the list of trees defined above.

    function addTreesRandomly( ... ) {
      ...
      var numberOfTreesAdded = 0;
      trees.forEach(function (tree) {
      });
    }
    

    In the iteration over trees, spawn a tree only with some probability and only if the number of trees added does not exceed 2. Update the for loop as follows.

    function addTreesRandomly( ... ) {
      ...
      trees.forEach(function (tree) {
        if (Math.random() 

    To conclude the function, return the number of trees added.

    function addTreesRandomly( ... ) {
      ...
      return numberOfTreesAdded;
    }
    

    Double check that your addTreesRandomly function matches the following.

    /**
     * Add any number of trees across different lanes, randomly.
     **/
    function addTreesRandomly(
      {
        probTreeLeft = 0.5,
        probTreeCenter = 0.5,
        probTreeRight = 0.5,
        maxNumberTrees = 2
      } = {}) {
    
      var trees = [
        {probability: probTreeLeft,   position_index: 0},
        {probability: probTreeCenter, position_index: 1},
        {probability: probTreeRight,  position_index: 2},
      ]
      shuffle(trees);
    
      var numberOfTreesAdded = 0;
      trees.forEach(function (tree) {
        if (Math.random() 

    Finally, to spawn trees automatically, setup a timer that runs triggers tree-spawning at regular intervals. Define the timer globally, and add a new teardown function for this timer.

    /*********
     * TREES *
     *********/
    ...
    var treeTimer;
    
    function setupTrees() {
    ...
    }
    
    function teardownTrees() {
      clearInterval(treeTimer);
    }
    

    Next, define a new function that initializes the timer and saves the timer in the previously-defined global variable. The below timer is run every half a second.

    function addTreesRandomlyLoop({intervalLength = 500} = {}) {
      treeTimer = setInterval(addTreesRandomly, intervalLength);
    }
    

    Finally, start the timer after the window has loaded, from the Game section.

    /********
     * GAME *
     ********/
    ...
    window.onload = function() {
      ...
      addTreesRandomlyLoop();
    }
    

    Navigate to your preview, and you’ll see trees spawning at random. Note that there are never three trees at once.

    Tree spawning at randomTree randomly spawning (Large preview)

    This concludes the obstacles step. We’ve successfully taken a number of template trees and generated an infinite number of obstacles from the templates. Our spawning algorithm also respects natural constraints in the game to make it playable.

    In the next step, let’s add collision testing.

    Step 3: Collision Testing

    In this section, we’ll implement the collision tests between the obstacles and the player. These collision tests are simpler than collision tests in most other games; however, the player only moves along the x-axis, so whenever a tree crosses the x-axis, check if the tree’s lane is the same as the player’s lane. We will implement this simple check for this game.

    Navigate to index.html, down to the TREES section. Here, we will add lane information to each of the trees. For each of the trees, add data-tree-position-index=, as follows. Additionally add class="tree", so that we can easily select all trees down the line:

    <a-entity data-tree-position-index="1" class="tree" id="template-tree-center" ...>
    </a-entity>
    <a-entity data-tree-position-index="0" class="tree" id="template-tree-left" ...>
    </a-entity>
    <a-entity data-tree-position-index="2" class="tree" id="template-tree-right" ...>
    </a-entity>
    

    Navigate to assets/ergo.js and invoke a new setupCollisions function in the GAME section. Additionally, define a new isGameRunning global variable that denotes whether or not an existing game is already running.

    /********
     * GAME *
     ********/
    
    var isGameRunning = false;
    
    setupControls();
    setupCollision();
    
    window.onload = function() {
    ...
    

    Define a new COLLISIONS section right after the TREES section but before the Game section. In this section, define the setupCollisions function.

    /*********
     * TREES *
     *********/
    
    ...
    
    /**************
     * COLLISIONS *
     **************/
    
    const POSITION_Z_OUT_OF_SIGHT = 1;
    const POSITION_Z_LINE_START = 0.6;
    const POSITION_Z_LINE_END = 0.7;
    
    function setupCollision() {
    }
    
    /********
     * GAME *
     ********/
    

    As before, we will register an AFRAME component and use the tick event listener to run code at every timestep. In this case, we will register a component with player and run checks against all trees in that listener:

    function setupCollisions() {
      AFRAME.registerComponent('player', {
        tick: function() {
          document.querySelectorAll('.tree').forEach(function(tree) {
          }
        }
      }
    }
    

    In the for loop, start by obtaining the tree’s relevant information:

     document.querySelectorAll('.tree').forEach(function(tree) {
      position = tree.getAttribute('position');
      tree_position_index = tree.getAttribute('data-tree-position-index');
      tree_id = tree.getAttribute('id');
    }
    

    Next, still within the for loop, remove the tree if it is out of sight, right after extracting the tree’s properties:

     document.querySelectorAll('.tree').forEach(function(tree) {
      ...
      if (position.z > POSITION_Z_OUT_OF_SIGHT) {
        removeTree(tree);
      }
    }
    

    Next, if there is no game running, do not check if there is a collision.

     document.querySelectorAll('.tree').forEach(function(tree) {
      if (!isGameRunning) return;
    }
    

    Finally (still in the for loop), check if the tree shares the same position at the same time with the player. If so, call a yet-to-be-defined gameOver function:

    document.querySelectorAll('.tree').forEach(function(tree) {
      ...
      if (POSITION_Z_LINE_START 

    Check that your setupCollisions function matches the following:

    function setupCollisions() {
      AFRAME.registerComponent('player', {
        tick: function() {
          document.querySelectorAll('.tree').forEach(function(tree) {
            position = tree.getAttribute('position');
            tree_position_index = tree.getAttribute('data-tree-position-index');
            tree_id = tree.getAttribute('id');
    
            if (position.z > POSITION_Z_OUT_OF_SIGHT) {
              removeTree(tree);
            }
    
            if (!isGameRunning) return;
    
            if (POSITION_Z_LINE_START 

    This concludes the collision setup. Now, we will add a few niceties to abstract away the startGame and gameOver sequences. Navigate to the GAME section. Update the window.onload block to match the following, replacing addTreesRandomlyLoop with a yet-to-be-defined startGame function.

    window.onload = function() {
      setupTrees();
      startGame();
    }
    

    Beneath the setup function invocations, create a new startGame function. This function will initialize the isGameRunning variable accordingly, and prevent redundant calls.

    window.onload = function() {
        ...
    }
    
    function startGame() {
      if (isGameRunning) return;
      isGameRunning = true;
    
      addTreesRandomlyLoop();
    }
    

    Finally, define gameOver, which will alert a “Game Over!” message for now.

    function startGame() {
        ...
    }
    
    function gameOver() {
      isGameRunning = false;
    
      alert('Game Over!');
      teardownTrees();
    }
    

    This concludes the collision testing section of the endless runner game.

    In this step, we again used A-Frame components and a number of other utilities that we added previously. We additionally re-organized and properly abstracted the game functions; we will subsequently augment these game functions to achieve a more complete game experience.

    Conclusion

    In part 1, we added VR-headset-friendly controls: Look left to move left, and right to move right. In this second part of the series, I’ve shown you how easy it can be to build a basic, functioning virtual reality game. We added game logic, so that the endless runner matches your expectations: run forever and have an endless series of dangerous obstacles fly at the player. Thus far, you have built a functioning game with keyboard-less support for virtual reality headsets.

    Here are additional resources for different VR controls and headsets:

    • A-Frame for VR Headsets
      A survey of browsers and headsets that A-Frame VR supports.
    • A-Frame for VR Controllers
      How A-Frame supports no controllers, 3DoF controllers and 6DoF controllers, in addition to other alternatives for interaction.

    In the next part, we will add a few finishing touches and synchronize game states, which move us one step closer to multiplayer games.

    Stay tuned for Part 3!

    Smashing Editorial
    (rb, ra, yk, il)

    From our sponsors: How To Build An Endless Runner Game In Virtual Reality (Part 2)

    Posted on 13th March 2019Web Design
    FacebookshareTwittertweetGoogle+share

    Related posts

    Archived
    22nd March 2023
    Archived
    18th March 2023
    Archived
    20th January 2023
    Thumbnail for 25788
    Handling Continuous Integration And Delivery With GitHub Actions
    19th October 2020
    Thumbnail for 25778
    A Monthly Update With New Guides And Community Resources
    19th October 2020
    Thumbnail for 25781
    Supercharge Testing React Applications With Wallaby.js
    19th October 2020
    Latest News
    • Archived
      22nd March 2023
    • Archived
      18th March 2023
    • Archived
      20th January 2023
    • 20201019 ML Brief
      19th October 2020
    • Thumbnail for 25788
      Handling Continuous Integration And Delivery With GitHub Actions
      19th October 2020
    • Thumbnail for 25786
      The Future of CX with Larry Ellison
      19th October 2020
    News Categories
    • Digital Marketing
    • Web Design

    Our services

    Website Design
    Website Design

    A website is an important part of any business. Professional website development is an essential element of a successful online business.

    We provide website design services for every type of website imaginable. We supply brochure websites, E-commerce websites, bespoke website design, custom website development and a range of website applications. We love developing websites, come and talk to us about your project and we will tailor make a solution to match your requirements.

    You can contact us by phone, email or send us a request through our online form and we can give you a call back.

    More Information

    Digital Marketing
    Digital Marketing

    Our digital marketeers have years of experience in developing and excuting digital marketing strategies. We can help you promote your business online with the most effective methods to achieve the greatest return for your marketing budget. We offer a full service with includes the following:

    1. Social Media Marketing

    2. Email & Newsletter Advertising

    3. PPC - Pay Per Click

    4. A range of other methods are available

    More Information

    SEO
    SEO Services

    SEO is an essential part of owning an online property. The higher up the search engines that your website appears, the more visitors you will have and therefore the greater the potential for more business and increased profits.

    We offer a range of SEO services and packages. Our packages are very popular due to the expanse of on-page and off-page SEO services that they cover. Contact us to discuss your website and the SEO services that would best suit to increase your websites ranking.

    More Information

    E-commerce
    E-commerce Websites

    E-commerce is a rapidly growing area with sales online increasing year on year. A professional E-commerce store online is essential to increase sales and is a reflection of your business to potential customers. We provide professional E-commerce websites custom built to meet our clients requirements.

    Starting to sell online can be a daunting task and we are here to make that journey as smooth as possible. When you work with Cunningham Web Solutions on your E-commerce website, you will benefit from the experience of our team and every detail from the website design to stock management is carefully planned and designed with you in mind.

    More Information

    Social Media Services
    Social Media Services

    Social Media is becoming an increasingly effective method of marketing online. The opportunities that social media marketing can offer are endless and when managed correctly can bring great benefits to every business.

    Social Media Marketing is a low cost form of advertising that continues to bring a very good ROI for our clients. In conjuction with excellent website development and SEO, social media marketing should be an essential part of every digital marketing strategy.

    We offer Social Media Management packages and we also offer Social Media Training to individuals and to companies. Contact us to find out more.

    More Information

    Cunningham Web Solutions
    © Copyright 2025 | Cunningham Web Solutions
    • Home
    • Our Services
    • FAQ's
    • Account Services
    • Privacy Policy
    • Contact Us