wanieru
Portfolio Profile Projects Main Site

Welcome!

My name is Daniel Hansen. This page is a collection of various projects I have worked on, both as the sole developer and in bigger teams. I have many years of experience with C#, JavaScript, PHP and more. I have worked a great deal in Unity, as well as built engines and frameworks from the bottom up. I am constantly experimenting with new things and I am always eager learn more.

Contact

If you want to reach me, you can do so at portfolio@wanieru.com. Enjoy your stay!
E-mail Me
9th March
2018

Treasure Hunters, Inc.

In this whiteboard platformer, you play as an intern who must deliver the batteries of a huge laser weapon to your boss before the treasure they're hunting escapes.
Download
In this whiteboard platformer, you play as an intern who must deliver the batteries of a huge laser weapon to your boss before the treasure they're hunting escapes.
This was our first project using MonoGame (although not my first experience with the framework). The point of the project was implementing certain design patterns, so we created a new engine called VikingEngine, which among others used the following design patterns:
  • Component Pattern: In previous projects, we let specialized game objects inherit from a central GameObject superclass, which would then let all gameobjects receive update and draw calls. In this project, we wanted to simplify the inheritance structure of the project, while still easily allowing to reuse behaviour. Thus, we implemented a component pattern, where all gameobject behaviour lives on components on a sealed game object class, meaning we no longer inherit game objects, but simply change behaviour by swapping out components.
  • Builder Pattern: This pattern ties nicely with the component pattern. Since what we consider a Game Object is a collection of components, we need an easy way to set up these components into a specific configuration. So builder classes specialize from a builder superclass, an can be used when adding the game objects to the world to make sure every instance of, for example, an enemy is set up in the same way.
  • Visitor Pattern: The physics system needs to write specialized code for every collision between two specific types (for example, circle-circle, rectangle-circle, rectangle-rectangle). However, the physics manager doesn't care what specialized shapes it's dealing with, it just wants to know if two arbitrary shapes are touching each other. To solve what seems like a conflict of interests, the shapes implement a visitor pattern. The physics manager is able to call the Visit function on the shape, which simply takes in another general shape. But the specific visited shape is aware about its own shape, and can then call the general shape's collision code overload for that specialized shape. At last, we have reached a function which is aware about the specialization of both shapes, and can then implement the specific math for that combination.

Programming

Art

  • Camilla Vallentin
  • Daniel Hansen

Level Design

  • Daniel Hansen
  • Camilla Vallentin

Sound Design

Voice Acting

  • Mikkel Lodahl as The Intern and The Boss
  • Nicklas Johansen Lið as The Treasure
Read More
28th September
2017

Terminal Chess

A chess implementation & AI, programmed in a Console Application. Made for a basic programming challenge, banning all use of classes, structs, lists, dictionaries, threading and so on...
Download
For my first programming project on the datamatiker school, our teachers wanted to keep everyone on equal footing. Therefore, only the basic programming techniques we were taught were allowed to be used for this project. Methods, enums and arrays. I wasn't allowed to use classes, objects, structs, lists and other advanced programming techniques, you'd normally want when structuring such a complicated game as chess.
One of the most interesting challenges when making this game was being able to keep multiple chess boards in memory and manipulate them. This is neccessary because:
  • A move is only legal if it doesn't result in your king being under attack. But how do you know without checking?
  • Meta rules such as three-fold repitition allows a player to claim a draw after a position has been exactly repeated three times. However, in order to implement this rule, I need to keep the full history of the game.
  • On the off-chance I wanted to make an AI, I needed to allow it to make fake moves, measure the new board's performance, and compare to determine the best move
So I need to be able to have multiple boards. In regular object-oriented programming, you want to make a "board"-class, which you can make multiple objects of, each representing a unique board configuration. But I couldn't do that. You would also want to put functions such as "move this piece" and "give me the list of legal moves" on this class, but I had to keep them all in my main Program class.
I think I came up with a pretty clever solution to all of these problems. I had enums for piece types and piece colors. (Even these, I would ideally want to put into a single structure, since they are typically paired to represent a single piece). But instead, I had a two-dimensional array for the piece color of every tile and a two-dimensional array for the piece type of every tile. I similarly had a field representing whose turn it is, which sides can castle, how many moves has been made and so on. All the information needed to represent a single chess board, just floating around here in the main class. But then, every method is able to manipulate and read these fields.
Now take all of these fields, and make them into an array! Of length 100. 100 elements in each. All the values at index 0 are the board values of board 0. When needing to just move a piece and check something, you can use a function to clone the values from index 0 to index 1, then do the move there. (Worth noting: I couldn't use lists, so I had to define a length of each array. However, I have a function which can recreate the arrays with a greater length and copy over all the old values.)
Effectively, I'm now passing around pointers to boards instead of board objects, and every function just manipulates the index of the board it's given. Boards can be cloned, and released when no longer used, so that index can be used by someone else. Boards also keep a reference to the index it was cloned from, so you can use that to trace back the history of a given game. And the AI can use it to test moves and consider its next decision.
So this is how I was able to make an easy to play console chess application. Using only basic programming techniques. There's an AI (only 3 ply, though, you can only get so much performance without threading), you can view the game history, and every single chess rule and meta rule is implemented properly.
Read More
4th September
2017

Enrolled as Datamatiker at Dania Games

I started studying to become a datamatiker at Dania Games in Grenaa. I'm making valuable experiences developing video games which are more than just school projects, they're interesting concepts, executed well under the constraints. I'm always trying to use interesting new technologies as part of my productions.
6th June
2017

Echo Cards

A flashcard app aimed at studying Japanese kanji and vocabulary using KanjiDamage. However, custom decks enable users to study anything by adding their own cards.
Visit
I've created quite a few desktop applications and web applications in the past to assist me in my study of Japanese. Each time, they grow in features, polish and scope. From small narrow flash card applications, to a large website with kanji handwriting. Echo Cards is the latest installment in this series. During my last trip to Japan, I was inspired to change the way I study Japanese. Partly due my experiences with the language while there, and partly due to seeing how my friends studied using various existing tools.
This time, I wanted to change my focus a bit. First of all, I was moving away from my unique selling point of my old system, DangoKanji. DangoKanji had an integrated Chrome Extension, which would prompt you to complete challenges during normal browsing of the internet. It was a way to integrate studying into the course of my day, and it served me well for some time. But I faced the reality, that absent mindedly answering these questions while in them iddle of something else doesn't provide the best memory retention, and the constant interruption got to me at times, resulting in long periods of time where I turned off the plugin and forgot all about it. I also had an attempt at spaced repitition, but it focused on repeating new kanji instead of weak kanji. I took the opportunity to automatically mark a challenge as "right" or "wrong" based on a multiple-choice system. This too proved counter-productive, as you become conditioned to simply answering the question, or recognizing how the correct answer looks, without exercising the brain muscles which makes you really learn the information.
Many friends of mine had on several occasions asked if I could adapt DangoKanji to work with other information - like maths or Chinese. I could was usually the answer, but not much else happened in the regard
All in all, I decided on a few specific things I wanted to focus on in this new iteration.
  • Session-based practice. Practice should happen in set time-slots to maximize focus on the task at hand. I decided to make a very dynamic and smooth practice system, which would use AJAX-requests to reduce load times and effectivise practice.
  • Include mnemonics. This would make me actually use the mnemonics and help with retaining the kanji.
  • Always require self-evaluation. Before I had self-evaluation when the user had to hand-write a kanji. I dreaded that type of challenge, because I couldn't as absent-mindedly choose the answer that looked correct and quickly move on. I had to think. In Echo Cards, there are no multiple choice questions, and you always have to do self-evaluation.
  • Allow custom decks. Allowing custom decks means anyone can use the gamification and system I create to practice anything. While Japanese and Piano Sheet Music are built-in decks, you can create custom decks to practice anything from programming to Game of Thrones House Sayings. I use it to add my own vocabulary.
  • Proper Spaced Repitition. This time I implemented a tried and tested repitition system, specifically the Leitner System.
The following code snippets reveal the basics of the dynamic practice system. An entire practice session happens from one page, dynamically contacting the server and loading the next challenge using AJAX-requests. To maximize code-reusage and minimize repetitive development, I made it easy for the client to send specific data to the server and the server to respond with raw html representing the result. Any button with a specific class sends a request to the server with that button's action. The server then decides what to do as a response, and returns a json_encoded array of the html panels to spit out as a result. These panels will then usually contain more buttons with other actions, and so the cycle repeats.
var panelInterval = 1500;
var currentlyPrinting = 0;
var loadingPractice = false;

$(document).on("click", ".actionButton[action]", function()
{
  //This generic bind to the click event means I can easily make buttons do actions that go by the server and come back, just by giving a button the class actionButton and an attribute with a string identifying the action.
  var action = $(this).attr("action");
  if($(this).hasClass("confirmation") && !confirm("Are you sure you want to perform this action?"))
    return;
  //Some actions are dangerous - the confirmation class easily puts a confirmation dialog before the action is exectued.
  DoPracticeAction(action);
});
function DoPracticeAction(action)
{
  if(currentlyPrinting > 0 || loadingPractice) //We block actions while awaiting response from the server, or animating new panels into existance.
    return;
  loadingPractice = true;
  AjaxParse("post/practiceaction.php", {"action" : action, "deck_id" :practiceDeckId}, "POST", function(data)
  {
    if(data["status"] == "ok")
    {
      SpitOutPanels(data["panels"]);
    }
    loadingPractice = false;
  });
}
function SpitOutPanels(array)
{
  var delay = 0;
  for(var panel in array)
  {
    //We immediately prime each panel to be printed a specific time - each with a set interval between - panelInterval.
    PracticeWell(array[panel], delay);
    delay += panelInterval;
  }
}
function PracticeWell(html, time)
{
  //To prime printing this well, an anonymous function is created with the html built into it, freeing up that variable name for the next function call which will occur before this panel is printed.
  setTimeout((function(inner_html){return function()
      {
        MakeWell(inner_html);
      };})(html), time);
  currentlyPrinting++;
}
function MakeWell(html)
{
  window.scrollTo(0, 0);
  //All previous panels' action buttons are disabled when a new one is printed, to avoid misunderstandings. The server always assumed an action is meant to be done on the last panel it printed.
  $("#practice_wells .actionButton[action]").removeAttr("action").removeClass("actionButton").addClass("disabled");
  $("#practice_wells").prepend(html);
  var newPanel = $("#practice_wells").children().first();
  newPanel.css({"opacity" : 0});
  newPanel.animate({"opacity" : 1}, 200, function()
  {
    $(this).removeAttr("style");
  });
  currentlyPrinting--;
}
$(document).on("click", ".challengePanel.challenge", function()
{
  //The practice panels have part of them hidden by default. These elements are hidden by having a "censored" class, which clicking the panel removes.
  $(this).find(".censored").removeClass("censored");
});
On the server, the requested action is interpreted. Sometimes, the action tells the server about the user's self-evaluation result. When that happens, the server awards XP to the user and creates a panel telling the user, that they gained XP. This code generates that panel using an array with info about the xp awarded and a function to create the panel's specific markup.
<?php
function PracticeXpPanel($xpResult)
{
  //Another function awards XP and returns an object with info on how it went. Was rested XP spent? Did the user level up? If so, what player icon did they get? etc..
  global $events;
  global $iconRarities;
  global $xpScale;
  //The user can get bonus XP for certain decks and bonus XP for being rested. The in-line if statements makes sure to inform the user if it happened.
  $xpContent = '<h1>You gained '.($xpResult['xp_awarded'] * $xpScale).' XP '.($xpResult['rested_used'] ? ' (X2 Rested Bonus)' : '').($xpResult['review_used'] ? ' (X2 Review Bonus)' : '').'!</h1>';
  $xpIcon = 'stars-stack';
  $xpClass = '';
  $xpDescription = 'XP Get!';
  $xpRank = 0;
  if($xpResult['level_up'])
  {
    //A lot of the content of this panel is changed if the user leveled up when gaining XP. This includes changing the icon to the player icon they just found, and the color to match its rarity.
    $xpRank = $xpResult['icon']['rarity'];
    $xpClass = 'rarityColor';
    $xpIcon = $xpResult['icon']['icon'];
    $xpDescription = $xpResult['icon']['title'];
    $rarity = $iconRarities[$xpRank]['title'];
    $event = '';
    if($xpResult['icon']['event'] != 'none')
    {
      $event = ' from the '.$events[$xpResult['icon']['event']]['title'].' event';
    }
    $duplicateString = $xpResult['duplicate'] ? ' (Dupicate)' : '';
    $xpContent .= "<h1>You leveled up - Level $xpResult[current_level]! You gained a new <span class=\"rarityColorText\" rank=$xpRank>$rarity</span> player icon$event: ".$xpResult['icon']['title']."$duplicateString</h1>";
  }
  return PracticePanel($xpIcon, $xpDescription, $xpContent, [], $xpClass, $xpRank);
}
Each core deck (and subsequently custom decks) is an object, which inherits from a base deck class, allowing most components of Echo Cards to be agnostic towards which deck it's dealing with. The code snippet below is an example of how other components interface with the decks, as well as how I use NotORM to interface with the database. The code generates the most important part of the practice loop, the actual challenge.
<?php
function PracticeChallenge($deck)
{
  global $db;
  global $loggedinid; //The id of the user, who's logged in
  global $progressRanks; //This array describes the Leitner boxes
  $newCard = $deck->GetChallengeCardId(); //The deck chooses a card for the user's challenge, and returns the ID.
  if($newCard < 0)
  {
    return PracticePanel('thumb-up', 'All Done!', '<h1>There are no more cards to review! Would you like to add some more cards to the deck?</h1>', [PracticeActionButton('stack', 'Learn More Cards', 'propose_next')]);
  }
  $card = $deck->GetCard($newCard);
  $progress = $deck->GetCardProgress($newCard);
  $db->nk_challenges()->insert(['user' => $loggedinid, 'deck' => $deck->DeckId(), 'card' => $newCard, 'result' => '']);
  $question = $card['question'];
  $answer = $card['answer'];
  $info = $card['info'];
  $content = "<h1 class=\"question\"><b>Q: </b>$question</h1>".
  "<div class=\"answer censored\"><h1><b>A: </b>$answer</h1><h3>$info</h3></div>".
  '<div class="progress progress-striped"><div class="timer timer-progress progress-bar progress-bar-info" style="width: 100%"></div></div>'.
  '<script>$(".timer-progress").css({"width":"100%"}).animate({"width" : "0%"}, 10000, "linear").removeClass("timer-progress");</script>';
  $actions =
  [
    PracticeActionButton('thumb-down', 'Couldn\'t recall', 'challenge_wrong', 'danger', 'censored'),
    PracticeActionButton('thumb-up', 'Correct', 'challenge_correct', 'success', 'censored'),
  ];
  $highestRank = max(array_keys($progressRanks)); //The code is agnostic to the id of the highest rank, so I can easily modify the amount of leitner boxes
  if($progress < $highestRank - 1) //The "challenge_easy" action moves a card up to boxes, so it has to be at least 2 boxes below the highest one.
    $actions[] = PracticeActionButton('cake-slice', 'Piece of cake', 'challenge_easy', 'success', 'censored');
  if($progress < $highestRank - 2)
    $actions[] = PracticeActionButton($progressRanks[$highestRank]['icon'], 'I\'m a '.$progressRanks[$highestRank]["title"], 'challenge_master', 'primary', 'censored rankColor confirmation', $highestRank);
  $actions[] = PracticeActionButton($progressRanks['-1']['icon'], 'Scrap Card', 'challenge_scrap', 'primary', 'censored rankColor confirmation', -1);

  return PracticePanel($progressRanks[$progress]['icon'], $progressRanks[$progress]['title'], $content, $actions, 'rankColor challenge', $progressRanks[$progress]['key']);
}
Read More
25th November
2016

QumuluZ

A unique trivia board game, which utilizes a digital platform to keep questions updated while using media and delivering tailored questions to every player.
Visit
QumuluZ was based on a single question: Why do we have to make due with old, outdated questions with our trivia games? Soon after followed the idea: Why has no one put the question part of a standard family trivia board game into the cloud and onto our smartphones? The idea was born, and soon I was brought in to the project to bring the online part of the game to life.
QumuluZ is a board game and a trivia quiz game as you know it. But we use a digital platform to serve the questions, instead of including thousands of small pieces of cardboard, which can't be updated or changed. We add new questions on a regular basis, and update the existing ones to always be factually accurate. Utilizing a digital platform allows us to minimize repeated questions a user gets, serve questions based on each user's age, and let the user purchase additional categories for the game from the comfort of their living room.
I've filled countless of the technical roles in the development of QumuluZ, such as web developer, tools programmer, graphics designer and social media manager. While I can't share code snippets from QumuluZ, I can share some technical details.
The game runs in a browser to maximize compatability while keeping development time low for a single developer. Using Ajax requests, the game dynamically allows the player to choose categories to play with, change players and their ages, and of course use the main game loop, consisting of spinning a wheel, choosing a category, and drawing a question/answer. Development started with a prototype of this game in 2015, and was then reworked from scratch in the summer 2016 before the game's release just in time for christmas. The game box contains a board and cloud-shaped pieces, as well as a 16-digit code which grants the user access to the game, as well as 4 free categories. More categories can be purchased before starting a new game.
Behind the scenes, I also developed an application to administer the game's categories, questions and media attached to the questions. This too was developed first as a prototype and then reworked when specifications became more clear. When writing a question, an admin will categorize the question in terms of categories, difficulty and agegroup. The questions then go through a QA-process using another specialized tool, to make sure they are correctly categorized. When questions are served, the agegroup is chosen based on a player's age and how close they are to the limits of that group. The game then also makes sure that a user won't get repeated questions before they have exhausted the entire stack of questions.
QumuluZ is entirely in Danish, and can be purchased and shipped to most places in Denmark. It is also currently available in 11 Bog & Idé stores with more underway.
Read More
6th October
2016

Software Engineer at QumuluZ

I left Kiloo to work full time on QumuluZ, which I had helped start in 2015. Now we worked on getting it ready to launch before the christmas sale started, and it has since become a major success, with over one hundred different users playing the board game on the most busy days.
8th August
2016

Quality Assurance at Kiloo

During an intense month, I worked as QA at Kiloo to help internalizing their QA-process. I was able to use my experience developing to effectively locate and report on bugs in an upcoming game, and it taught me a lot about the development process from a new point of view.
20th June
2016

Graduated from GameIT College

GameIT College in Denmark is a gymnasium with a particular focus on video game development, which gave me many valuable experiences with video game development and a strong network.
18th May
2016

Pac-Table

The classic Pac-Man recreated as a table using simplistic LED-strips.
Pac-Table was our final project for our electronics class. Using an Arduino and LED-strips mapped out as a pac-man maze, we made a table which you can play Pac-Man on. We use a buzzer and the Arduino's built-in timer module to generate square wave sounds to go with the game. We paid close attention to detail, making sure that every one of the four ghosts appear and behave exactly as they did in the original Pac-Man. So while the maze-layout has been simplified a bit, veterans of the original should still be able to take advantage of the AI (even down to an overflow error with one of the ghosts being simulated in this version).
This was not just a school project - we put a lot of effort into making sure the product could be displayed and used by people every day, and it is now a permanent installation at the school.
Below are a few snippets of the code. Each ghost has its own set of variables, and use a collection of general functions to share logic. The following function is the general update function for a ghost. Its target and speed depends on the game's mode (chase / scatter mode, if you know your pac-man terminology), and if the ghost is dead or not. Another function is then responsible for not just moving ghosts but any character including pac-man, to make sure they move on fixed intervals based on their speed, and that they can't go through walls and so on. Finally, when a ghost moves, it runs the pathfinding function to find the next direction to move.
void GhostUpdate(boolean& isDead, int8_t target[], int8_t chaseTarget[], int8_t scatterTarget[], uint16_t& moveTimer, int8_t pos[], int8_t dir[], boolean& isFrightened)
{
    if (isDead) //Dead target
    {
        target[0] = GhostStartPos[0];
        target[1] = GhostStartPos[1];
    }
    else if (GhostModeChasing) //Chasing target
    {
        target[0] = chaseTarget[0];
        target[1] = chaseTarget[1];
    }
    else //Scatter target
    {
        target[0] = scatterTarget[0];
        target[1] = scatterTarget[1];
    }
    uint16_t speed = isDead ? GhostSpeedDead : (isFrightened ? GhostSpeedFrightened : GhostSpeed);
    if (CharacterMovement(moveTimer, speed, pos, dir) || (dir[0] == 0 && dir[1] == 0)) 
	{
        GhostPathfinding(pos, dir, target, isFrightened);
        if (isDead && pos[0] == GhostStartPos[0] && pos[1] == GhostStartPos[1])
        {
            isDead = false;
            dir[0] = 0;
            dir[1] = -1; //Ghosts turn left when leaving ghost house
        }
    }
}
At first, I was concerned about the Arduino's processing power to perform the ghost's pathfinding. But luckily, the orignal designers of Pac-Man had a similar issue, also running the game on relatively simple hardware, so the pathfinding algorithm is simple but effective. If a ghost is frightened, it takes all turns at random. But otherwise, it takes the turn that immediately brings it closest to its target, except that it can't turn 180 degrees, preventing it moving back and forth in a hallway. Because of the Pac-Man maze's layout, it is always able to find its way eventually, usually pretty efficiently.
void GhostPathfinding(int8_t pos[], int8_t dir[], int8_t target[], boolean frightened)
{
    int8_t targetCopy[] = { target[0], target[1] };
    if (frightened) {
        targetCopy[0] = random(0, MapHeight);
        targetCopy[1] = random(0, MapWidth);
    }
    int16_t currentShortest = 32767;
    int8_t newDir[] = { dir[0], dir[1] };
    for (int8_t y = -1; y < 2; y++) {
        for (int8_t x = ((y) ? 0 : -1); x < 2; x += 2) //If y is 0, x will be -1 and then 1. Otherwise, x will just be 0. The result, is that we loop through all 4 directions: (0,-1), (-1,0), (1,0), (0,1)
        {
            if (!IsPositionValid(pos, y, x))
                continue;
            if (dir[0] == -y && dir[1] == -x) //You can't turn around 180 degrees
                continue;
            int16_t newDistance = DistanceFromTarget(pos, y, x, targetCopy);
            if (newDistance < currentShortest) {
                currentShortest = newDistance;
                newDir[0] = y;
                newDir[1] = x;
            }
        }
    }
    dir[0] = newDir[0];
    dir[1] = newDir[1];
}

Programming

  • Daniel Hansen
1 / 5
The arcade machine as it was handed in.
Read More
30th March
2016

Solar Sailors

An intense party game, in which three players cooperate to steer a wooden ship in space towards a treasure.
Download
Solar Sailors is the product of the biggest game project I did during my time at GameIT College. On our senior year, we got together in large groups of 10 people, to work with a company structure to create a game in Unity with a big scope. Our game was a three-player coop game, which put the players on a wooden ship in outer space. The ship is controlled via 6 different tasks:
  • A rudder, to control the ship
  • Cannons, to fire at enemy ships
  • Lookout Post, to see the surrounding terrain
  • A Map, used to navigate to the treasure
  • Sails, used to adjust the ship's movement- and rotation speed
  • Fire, that needs to be put out to keep the ship afloat
But with only three players, the challenge is managing all 6 tasks at the same time. It requires the players to have on-point communication and a sense of urgency. There are many different components to the game. I was the lead programmer on the project, which included responsibilities such as making sure all the code would work together, and that our team of 6 programmers would reach milestones in time and always be able to solve their tasks. I also did a large part of the raw programming work. In the following I'll showcase the code for the AI. All ships, both player and AI, have the same "body". Same sprite, same collider, and a Ship Brain-script, which some other controller can tell to turn sails on, shoot cannons or turn the ship. When players interact with the ship's station, it goes through the ship brain to perform the actions in the game world. And similarly, the AI also just contacts the ship-brain directly to tell it what it wants the ship to do.
using UnityEngine;
using System.Collections;
using System.Linq;

public class ShipAI : Photon.MonoBehaviour
//All classes in this project inherit from Photon's MonoBehaviour. Photon is our networking library, so this gives us both an interface for the game world, but also an interface for networking
{
   private ShipBrain shipBrain;
   private Vector3 targetPoint; //The ship usually has somewhere in mind it wants to go, and this is it

   //The following are public variables that we can adjust from Unity's inspector, about the ship's ability to look ahead.
   public float HP = 15f; //The AI is responsible for killing off the ship when its health reaches 0.
   public float TargetDistanceFromPlayer = 50f;
   public float LookAheadRaycast = 60f;
   public float CannonRaycastDistance = 30f;
   public float MaxDistanceFromTarget = 300f;
   public float FastChaseXMargin = 10f;

   void Start ()
   {
      shipBrain = GetComponent<ShipBrain>();
      targetPoint = Vector3.zero;
   }

   void Update ()
   {
      AILogic();
   }

   void Die()
   {
      if (!photonView.isMine) //One of the clients is the game "master". On the Photon network, he owns all of the AI-ships, and so this statement prevents the other clients from attempting to network-destroy the ship. They'll wait for the master-client to tell them about it.
         return;
      GameManager.ST.photonView.RPC("SpawnDeathParticleSystem", PhotonTargets.All, transform.position);
      PhotonNetwork.Destroy(photonView);
   }
   void SetSails(int sails)
   {
      //Here, the strength of the controller-brain structure shines through. The AI-ship simply asks the shipBrain to toggle its sails, and then the ship brain handles the networking, regardless of it being an AI-ship or the player-ship.
      //This function turns on a set amount of sails. There are three pairs of sails that can be turned on and off. Luckily, the AI doesn't need to run around to toggle them.
      for (var i = 0; i < shipBrain.SailArray.Length; i++)
      {
         var newState =  i < sails;
         if(shipBrain.SailArray[i] != newState)
            shipBrain.ToggleSail(i);
      }
   }
   void OnTriggerEnter2D(Collider2D other)
   {
      if (!photonView.isMine)
         return;
      var cannonBullet = other.GetComponent<CannonBullet>();
      if (cannonBullet != null && cannonBullet.IsPlayerCannonBall)
      {
        //If we're here, the AI-ship has just gotten hit by one of the player's cannon balls. To start off, we transfer ownership of the cannonball to the master-player, so he can network destroy them.
         cannonBullet.photonView.TransferOwnership(PhotonNetwork.player);
         PhotonNetwork.Destroy(cannonBullet.photonView);
         HP -= 1f;
         PhotonNetwork.Instantiate("Prefabs/Game/AI/FakeFire", shipBrain.RandomFirePosition(), Quaternion.identity, 0, new object[]{photonView.viewID});
        //On the player ship, very real fires are spawned, that actually kills their ship if left untended to. The AI doesn't bother to put out fires, and because we have a seperate tally of its HP here, we spawn fake fires as a visual effect.
         photonView.RPC("PlayImpactSound", PhotonTargets.All);
      }
   }
   [PunRPC]
   void PlayImpactSound()
   {
      SoundEffectPlayer.PlaySoundHere("Impact", transform.position);
   }
}
The above code runs through basic functions of the AI, but the meat of its behaviour is in the AILogic-function, which I'll showcase below. The AI has four primary functions in its behaviour:
  • Sail towards the player ship
  • Avoid sailing into planets and other obstacles on the way
  • Make itself parallel witht the player ship, such that its cannons are pointing at it
  • Fire the cannons when they are in range
This is accomplished basically with a raycast towards the player, to see if the path to them is not currently obstructed, and a raycast in the direction of the AI-ship, to make sure its not about to crash into something. The AI isn't actually punished for going through obstacles like the players are, but this behaviour allows players to juke and lose the AI-ships.
void AILogic()
{
  if (!photonView.isMine)
    return; //AI Logic is only performed by the master client
  if (ShipBrain.PlayerShip != null)
  {
    var playerShipPosition = ShipBrain.PlayerShip.transform.position;
    targetPoint = playerShipPosition + ((transform.position - playerShipPosition).normalized * TargetDistanceFromPlayer); //The target point is TargetDistanceFromPlayer units from the player ship in the current direction of the enemy ship.
  }

  var frontRaycast = Physics2D.RaycastAll(transform.position, transform.TransformDirection(Vector3.up), LookAheadRaycast, LayerMask.NameToLayer("World")); //Makes raycast in front of the ship.
  var targetRaycast = Physics2D.RaycastAll(transform.position, targetPoint - transform.position, LookAheadRaycast, LayerMask.NameToLayer("World")); //Makes raycast towards the target point.

  var targetVector = targetPoint - transform.position;
  var localTargetVector = transform.InverseTransformDirection(targetVector); //The ship rotates, and it's sometimes helpful to have the vector towards the target be in local-space instead of world-space.

  if (frontRaycast.Any(raycast => raycast.collider.tag == "WorldObject") || localTargetVector.sqrMagnitude < TargetDistanceFromPlayer * TargetDistanceFromPlayer)
  {
    //There's an object in front of us!!
    //OR, we're close enough to the player, that we want to turn to make us parallel with them
    SetSails(0); //Turn off the sails to minimize speed and maximize rotation capabilities
    shipBrain.Rotate(1); //Rotate in the arbitrary direction 1, to get away from this obstacle. The ship has no concern for which rotation direction would be optimal.
  }
  else if (targetRaycast.All(raycast => raycast.collider.tag != "WorldObject"))
  {
    //Since we're here, there's no object immediately in front of us.
    //There are also no objects between us and the goal. Let's rotate back towards the goal.

    var angle = Vector3.Angle(localTargetVector, Vector3.up);
    //Now we have a local vector towards the target in local space. That must mean, that the x-coordinate's sign can dictate the rotation direction.
    var rotationDirection = Mathf.Sign(localTargetVector.x)*-1;
    if (Mathf.Abs(rotationDirection) <= 0.01f) //You always have to rotate in this state, in case the target is actually directly behind you!!
      rotationDirection = 1f;

    SetSails(angle < FastChaseXMargin ? 3 : 0); //Within some margin of the x-component, the ship turns on all sails to quickly chase the target ship. But if the x-component is too big, the sails are turned off to focus on turning the ship in the right direction.
    shipBrain.Rotate(rotationDirection);
  }
  else
  {
    //There is an object between us and the target, but not in front of us. Let's keep going forward at an average speed, hoping that one day the path to the target will be clear again.
    SetSails(1);
  }

  foreach (var cannon in shipBrain.Cannons)
  {
    if(ShipBrain.PlayerShip == null)
      break;
    //For each cannon that is able to fire, a raycast checks in the cannon's fire direction if it would hit the player. If this is the case, the cannon is immediately told to fire. The cannon's behaviour script handles the networking.
    if (!cannon.CanFire)
      continue;
    var cannonRaycast = Physics2D.RaycastAll(cannon.transform.position, cannon.transform.TransformDirection(Vector2.up), CannonRaycastDistance, LayerMask.GetMask("World"));
    if(cannonRaycast.Any(raycast => raycast.transform == ShipBrain.PlayerShip.transform))
      cannon.Activated();
  }

  if (HP <= 0.0001f)
  {
    Die();
  }

  if ((targetPoint - transform.position).sqrMagnitude > (MaxDistanceFromTarget * MaxDistanceFromTarget))
  {
    //There is a max distance from the player, that if the AI exceeds, it will immediately self-destruct instead of trying to navigate back there. The max distance is squared, to save a square-root operation on the left-hand side of the inequality.
    Die();
  }
}
Solar Sailors was arguably the most fun and polished product to come out of the 3-month project. It got really good remarks by our judges, and a few months later, we discovered a newly announced game called Sea of Thieves with an uncanny amount of similarity to Solar Sailors, from theme and gameplay to presentation.
Read More
27th November
2015

Associate Software Engineer at Kiloo

For 5 months, I worked as an associate software engineer while studying. I worked closely with the CCO of Kiloo to create a prototype for a new game idea, which taught me a lot about working under pressure, as we had to make deadlines while studying on the side.
Load More
Copyright © Wanieru 2018