About Me

Hello!

It's-a me, Jannis Harder, a Gameplay Programmer from Germany. I am a game and coding enthusiast, working professionally on games since 2016. I am always eager to learn new things and improve myself to make the best looking and feeling games writing clean code.

After working as an independent game creator and self-publisher for a few years I started working for Ubisoft Blue Byte Mainz on the Anno franchise. Find out more about my work below.

Skills

  • Languages: C/C++, C#, Python, Lua, JavaScript, HTML5, CSS, PHP
  • Engines: Unreal Engine 4, Unity Engine
  • Software: Visual Studio, VS Code, Blender, Audacity, Photoshop, Premiere
  • Others: Perforce, Git, MySQL

Projects

Anno 1800 Console Edition
  • Ubisoft Blue Byte Mainz - published
  • PS5 / XBox Series X|S - C++
In Anno 1800 you are able to design huge metropolises, manage an emerging economy and protect your creations from others.
To let your cities flourish, you'll have to learn to take the right measures in any situation.

Anno 1800 offers you plenty of opportunities to prove your skills.
Here you'll be able to build huge cities, plan logistic networks, colonize new fertile continents, undertake expeditions around the globe
and dominate your opponents through diplomacy, trade or warfare.

My Tasks

  • Controller Input
  • Build Modes and Build Tools
  • Naval Combat
  • UI Programming
Website: ubisoft.com/de-de/game/anno/1800/console-edition
Myràd
  • 2 people
  • 3 years
  • MAJA Studios - published
  • MAJA Engine - JavaScript
Myràd is a single player, point-and-click roleplay game.

It was the first game released by MAJA Studios, a company me and a good colleague of me founded.
Myràd was developed parallel to the MAJA engine which is a browser-based engine, fully functional on PCs, mobile phones and tablets.

It was released in 2019 and currently has around 500 players.

Game Features

  • Compelling Story
  • Resources for Camp Building
  • RPG Battle System
  • Recruits for Battle Ship
  • Crafting / Brewing / Farming
  • Battle Story + Daily Quests
  • World Map + Trading System
  • Chose path of Trader or Pirate
Website: myrad-game.de
JavaScript Code Example - A* Pathfinding in a triangulated polygon
/**
    * Find shortest path in given polygon
    * with A* algorithm using funnel path portals
    * 
    * @param {float[]} fromPoint -  from 2D point
    * @param {float[]} toPoint - to 2D point
    * @returns {float[float[]]} array of 2D points
*/
MajaPolygon.prototype.getShortestPath = function(fromPoint, toPoint){
    if(this.triangulation === null){
        //triangulate if necessary
        this.calculateTriangulation();
    }
    
    //get from/to vertex data and ensure points are in polygon
    var closestStartValues = this.getClosestTriangleVertex(fromPoint);
    var startVertex = closestStartValues[0];
    fromPoint = closestStartValues[1];

    var closestEndValues = this.getClosestTriangleVertex(toPoint);
    var endVertex = closestEndValues[0];
    toPoint = closestEndValues[1];

    if(startVertex !== false && endVertex !== false){
        if(startVertex == endVertex){
            //from/to lie in same triangle, return direct path
            return [fromPoint, toPoint];
        }

        var closedSet = [];
        var openSet = [startVertex];

        var cameFrom = {};

        //use direct distance as score estimation
        var fScore = {};
        fScore[startVertex] = $g.func.getPointDistance(fromPoint, toPoint);

        while(openSet.length > 0){
            var currentNode = false;
            var currentNodeIndex = false;
            var currentNodeValue = false;

            //get next node with lowest estimated distance value
            for(var i=0;i<openSet.length;i++){
                var nodeValue = fScore[openSet[i]];
                if(
                    currentNodeValue === false ||
                    nodeValue < currentNodeValue
                ){
                    currentNode = openSet[i];
                    currentNodeIndex = i;
                    currentNodeValue = nodeValue;
                }
            }

            if(currentNode == endVertex){
                //shortest path determined
                return cameFrom[currentNode][1];
            }

            $g.func.removeFromArray(openSet, currentNodeIndex);
            closedSet.push(currentNode);

            for(var i=0;i<this.triangulationConnections.length;i+=2){
                //find neighbour node to given vertex
                var neighbourNode = false;
                if(this.triangulationConnections[i] == currentNode){
                    neighbourNode = this.triangulationConnections[i+1];

                } else if(this.triangulationConnections[i+1] == currentNode){
                    neighbourNode = this.triangulationConnections[i];
                }

                if(
                    neighbourNode !== false &&
                    closedSet.indexOf(neighbourNode) == -1
                ){
                    //calculate new funnel portal
                    var fromVertices = [];
                    fromVertices.push(this.triangulation[currentNode*3]);
                    fromVertices.push(this.triangulation[(currentNode*3)+1]);
                    fromVertices.push(this.triangulation[(currentNode*3)+2]);

                    //find shared portal vertices with neighbour triangle
                    var sharedVerticePoints = [];
                    var toVerticePoint;
                    for(var j=0;j<3;j++){
                        var neighbourVertice = this.triangulation[neighbourNode*3+j];

                        var fromVerticeIndex = fromVertices.indexOf(neighbourVertice);
                        if(fromVerticeIndex == -1){
                            //neighbour vertex is not in portal
                            toVerticePoint = [
                                this.flatArray[(neighbourVertice*2)],
                                this.flatArray[(neighbourVertice*2)+1]
                            ];

                        } else {
                            //neighbour vertex is part of portal
                            var sharedVertice = $g.func.removeFromArray(
                                fromVertices, fromVerticeIndex
                            );
                            sharedVerticePoints.push([
                                this.flatArray[(sharedVertice*2)],
                                this.flatArray[(sharedVertice*2)+1]
                            ]);
                        }
                    }
                    var leftVertice = fromVertices.shift();
                    var fromVerticePoint = [
                        this.flatArray[(leftVertice*2)],
                        this.flatArray[(leftVertice*2)+1]
                    ];

                    var cproduct_1 = $g.func.crossProduct(
                        fromVerticePoint, sharedVerticePoints[0], toVerticePoint
                    );
                    var cproduct_2 = $g.func.crossProduct(
                        fromVerticePoint, sharedVerticePoints[1], toVerticePoint
                    );

                    //create portal sorted left/right vertex
                    var newPortal;
                    if(cproduct_1 < cproduct_2){
                        newPortal = [sharedVerticePoints[0], sharedVerticePoints[1]];

                    } else {
                        newPortal = [sharedVerticePoints[1], sharedVerticePoints[0]];
                    }

                    var atEndVertice = (neighbourNode == endVertex);

                    //extend or start funnel path algorithm through given portal
                    var aStarFunnelPath, tentativePath;
                    if(currentNode in cameFrom){
                        aStarFunnelPath = cameFrom[currentNode][0].getCopy();
                        tentativePath = aStarFunnelPath.extend(newPortal, atEndVertice);

                    } else {
                        aStarFunnelPath = new $g.func.AStarFunnelPath(
                            fromPoint, toPoint, [newPortal]
                        );
                        tentativePath = aStarFunnelPath.calculate(atEndVertice);
                    }

                    //get funnel path from start to neighbour node
                    var tentativeGScore = $g.func.getPathLength(tentativePath);
                    
                    if(openSet.indexOf(neighbourNode) == -1){
                        openSet.push(neighbourNode);

                    } else if(tentativeGScore >= fScore[neighbourNode]){
                        continue;
                    }

                    //store funnel path length as fScore(=gScore)
                    cameFrom[neighbourNode] = [aStarFunnelPath, tentativePath];
                    fScore[neighbourNode] = tentativeGScore;
                }
            }
        }

    } else {
        $g.log_e(
            "getShortestPath",
            "could not calculate start or end "+startVertex+", "+endVertex
        );
    }

    return [];
}
The Hive
  • 1 person
  • 1 week
  • private project - prototype
  • Unreal Engine - C++
The Hive is a prototype I created for Brackeys Game Jam 2021.1. The theme is "Stronger Together".
The game is about controlling a swarm of bees to collect nectar, create honey and ensure the surviving of the hive.

Game Features

  • Find blossoms to collect nectar
  • Dance to show other bees the way to known blossoms
  • Spawn and switch between all bees in your hive
  • Eat honey to keep your bees power up to prevent its death
  • Stay home at night, the colder it gets, the faster the bees power drains
  • Stay away from wildlife or the bee stings and dies
Website: itch.io/the-hive
C++ Code Example - Bee Pawn Tick and NPC action
void ABeePawn::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    bool IsInMenu = (GetWorld()->GetAuthGameMode<ATheHiveGameModeBase>())->IsInMenu;

    if (!IsInMenu)
    {
        //remove bee power based on day state
        if ((GetWorld()->GetGameState<ABeeGameStateBase>())->IsNight)
        {
            RemovePower(tickPowerCostsNight);
        }
        else
        {
            RemovePower(tickPowerCosts);
        }
    }

    if (!IsInMenu && RestrictMovementTime <= 0.f)
    {
        //update rotation
        FRotator NewRotation = GetActorRotation();
        NewRotation.Yaw += CameraInput.X;
        NewRotation.Pitch = FMath::Clamp(
            NewRotation.Pitch + CameraInput.Y, -60.f, 89.9f
        );
        SetActorRotation(NewRotation);
    }

    if (IsAlive)
    {
        if (RestrictMovementTime > 0.f)
        {
            //keep movement restricted
            RestrictMovementTime -= DeltaTime;
            MovementInput = FVector(0.f);
        }
        else
        {
            CurrentBeeStatus = BeeStatus::Normal;

            //check npc actions
            if (NpcControlled)
            {
                if (HasTarget)
                {
                    if (
                        (TargetPosition - GetActorLocation()).SizeSquared() <=
                        TargetMinSqrDist
                    )
                    {
                        //bee arrived at destination, update target
                        HasTarget = false;

                        if (NpcTarget == BeeNpcTarget::Flower)
                        {
                            ++NpcFlowerVisits;
                            NpcTarget = BeeNpcTarget::Nothing;
                        }
                        else if (NpcTarget == BeeNpcTarget::Hive)
                        {
                            NpcTarget = BeeNpcTarget::Nothing;
                        }
                    }
                }
                if (!HasTarget)
                {
                    //update target if bee has none
                    CheckNpcAction();
                }

                if (HasTarget)
                {
                    //move npc bee towards target
                    MovementInput =
                        (TargetPosition - GetActorLocation()).GetClampedToMaxSize(MaxSpeed);
                }
            }
        }
    }
    else {
        MovementInput = FVector(0.f, 0.f, -9.81f);
    }

    if (OurMovementComponent && (OurMovementComponent->UpdatedComponent == RootComponent))
    {
        //update movement components velocity
        float UseAccelerationDelta = (CurrentVelocity.SizeSquared() > MovementInput.SizeSquared()) ?
            DeaccelerationDelta : AccelerationDelta;

        CurrentVelocity = FMath::Lerp<FVector>(
            CurrentVelocity, MovementInput, UseAccelerationDelta * DeltaTime
        );
        OurMovementComponent->CurrentVelocity = CurrentVelocity;

        if (NpcControlled && HasTarget)
        {
            //update npcs bee rotation
            SetActorRotation(CurrentVelocity.Rotation());
        }
    }

    MovementInput = FVector(0.f);
}

void ABeePawn::CheckNpcAction()
{
    ABeeGameStateBase* BeeGameState = GetWorld()->GetGameState<ABeeGameStateBase>();

    if (NpcTarget == BeeNpcTarget::HiveEnter)
    {
        //move into hive
        TargetPosition = BeeGameState->HiveCenterPos +
            FMath::VRand() * FMath::RandRange(0.f, HiveCenterRadius);
        HasTarget = true;

        NpcTarget = BeeNpcTarget::Hive;
    }
    else if (IsInHive && Pollen > 0.f)
    {
        PassPollenToHive();
    }
    else if (IsInHive && Power < 0.8f && BeeGameState->HiveHoney > 0.f)
    {
        ConsumeHiveHoney();
    }
    else if (Pollen < 1.f && IsAtFlower != nullptr && IsAtFlower->PollenLeft > 0.f)
    {
        CollectFlowerPollen();
    }
    else if (Pollen > 0.75f || NpcFlowerVisits >= 3 ||
        BeeGameState->IsNight && !IsInHive
    )
    {
        //return to hive, move to hive entry first
        NpcFlowerVisits = 0;

        TargetPosition = BeeGameState->HiveEnterPos +
            FMath::VRand() * FMath::RandRange(0.f, HiveEnterRadius);
        HasTarget = true;

        NpcTarget = BeeNpcTarget::HiveEnter;
    }
    else if (KnowsAboutFlowers.Num() > 0 && !BeeGameState->IsNight)
    {
        //bee is in idle, check next action
        if (NearbyBeePawns.Num() > 0 && FMath::FRandRange(0.f, 1.f) > 0.8f)
        {
            //show nearby bees way to known flowers
            DoFlowerDance();
        }
        else
        {
            if (IsInHive)
            {
                //fly out of hive
                TargetPosition = BeeGameState->HiveEnterPos +
                    FMath::VRand() * FMath::RandRange(0.f, HiveEnterRadius);
                HasTarget = true;
            }
            else
            {
                //fly to random known flower
                AFlower* MoveToFlower = KnowsAboutFlowers[rand()% KnowsAboutFlowers.Num()];

                FBox ColliderBox = MoveToFlower->BoxCollider->Bounds.GetBox();
                TargetPosition = ColliderBox.GetCenter() +
                    FMath::VRand() * FMath::RandRange(0.f, FlowerRadius);
                HasTarget = true;

                NpcTarget = BeeNpcTarget::Flower;
            }
        }
    }
}
Blueprint Code Example - Fox AI Movement
"Karate Kid"
  • 3 people
  • 4 months
  • private project - in production
  • Unity Engine - C#
"Karate Kid" is the project title of a game for mobile phones I am working on within a team of 3 people - an artist and a game designer.
It is still in production and has, as a hobby project, no soon to come release date.

Game Features

  • Customize your Fighter
  • Learn Attack and Defensive skills
  • Use physical and mental training to increase your power/spirit
  • Fight against bosses to increase your belt level
  • Play against players around the world to increase your ranking
C# Code Example - Handle Fighter Animations
public IEnumerator StartGymAnimation(PlayerTimer activeTimer){
    System.Type timerType = (activeTimer != null) ?
        activeTimer.GetType() : null;

    //prepare animations
    if(timerType != null){
        if(timerType == typeof(PlayerTimerTrainingSkill)){
            if(CheckPlayerFighterStatus(
                FighterState.IDLE, GameController.staticStateNames["to_idle"].stateName
            )){
                yield return new WaitForSeconds(1f);
            }

            BattleFighter.UpdateFighterState(FighterState.TRAINING);

            TrainingSkill trainingSkill =
                ((PlayerTimerTrainingSkill)activeTimer).GetTrainingSkill();
            string prepareStateName =
                trainingSkill.GetPrepareAnimationStateName();
            if(prepareStateName != ""){
                BattleFighter.FadeToAnimationState(prepareStateName, 0.15f);
                yield return new WaitForSeconds(1f);
            }

        } else {
            if(CheckPlayerFighterStatus(
                FighterState.STANCE,
                GameController.staticStateNames["to_stance"].stateName
            )){
                yield return new WaitForSeconds(1f);
            }
        }

    } else {
        if(CheckPlayerFighterStatus(
            FighterState.IDLE,
            GameController.staticStateNames["to_idle"].stateName
        )){
            yield return new WaitForSeconds(1f);
        }
    }

    //start animations
    if(timerType != null){
        if(timerType == typeof(PlayerTimerDefenseSkill)){
            DefenseSkill defenseSkill =
                ((PlayerTimerDefenseSkill)activeTimer).GetDefenseSkill();
            if(defenseSkill != activePartnerTrainingDefenseSkill){
                activePartnerTrainingDefenseSkill = defenseSkill;

                List<KeyValuePair<SkillAnimationState, Attack>> defenseSkillAttacks =
                    new List<KeyValuePair<SkillAnimationState, Attack>>();
                foreach(
                    SkillAnimationState skillAnimationState in
                    defenseSkill.skillAnimationStates
                ){
                    foreach(Attack attack in GameModel.attacks){
                        if(
                            attack.GetAttackCategory() ==
                            defenseSkill.GetAnswerToAttackCategory() &&
                            attack.GetAttackPosition() ==
                            skillAnimationState.GetRestrictedToAttackPosition()
                        ){
                            defenseSkillAttacks.Add(
                                new KeyValuePair<SkillAnimationState, Attack>(
                                    skillAnimationState, attack
                                )
                            );
                        }
                    }
                }

                partnerTrainingCoroutine = Game.StartSync(
                    ShowPartnerTraining(defenseSkill, defenseSkillAttacks)
                );
            }

        } else {
            activePartnerTrainingDefenseSkill = null;

            List<AnimationScript> gymAnimationScripts =
                activeTimer.GetGymAnimationScripts();
            if(gymAnimationScripts.Count > 0){
                BattleFighter.StartAnimationScript(
                    gymAnimationScripts[UnityEngine.Random.Range(
                        0, gymAnimationScripts.Count
                    )].GetIdentifier(), true
                );

            } else {
                float minRepeatTime = 1.2f;
                float maxRepeatTime = 1.8f;

                List<SkillAnimationState> animationStateNames =
                    activeTimer.GetSkillAnimationStates(true, true);
                BattleFighter.RepeatTrainingAnimationState(
                    GameController.staticStateNames["stance"], animationStateNames,
                    minRepeatTime, maxRepeatTime
                );
            }
        }

    } else {
        activePartnerTrainingDefenseSkill = null;

        List<SkillAnimationState> randomIdleAnimationStates =
            new List<SkillAnimationState>(){
                GameController.staticStateNames["random_idle_1"],
                GameController.staticStateNames["random_idle_2"],
                GameController.staticStateNames["random_idle_3"]
        };
        BattleFighter.RepeatTrainingAnimationState(
            GameController.staticStateNames["idle"], randomIdleAnimationStates,
            5f, 8f
        );
    }
}
"Water League"
  • 1 person
  • 1 week
  • private project - prototype
  • Unity Engine - C#
Water League is a proof-of-concept prototype which is based on the popular game "Rocket League".
It has the same mechanics, with only half of the arena being underwater.

Game Features

  • Use Boost to dive underwater and fly through the air
  • Use the cars body to move the ball
  • Shoot inside your enemies' goal to score
C# Code Example - Process Car Input
public void ProcessInput(){
    float driftInput = Input.GetAxis("Drift");
    float accelerateInput = Input.GetAxis("Accelerate");
    float verticalInput = Input.GetAxis("Vertical");
    float horizontalInput = Input.GetAxis("Horizontal");
    float boostInput = Input.GetAxis("Boost");
    float rollInput = Input.GetAxis("Roll");

    float driftStiffness = (driftInput != 0) ? 0.5f : 2f;

    WheelHit wheelHit;
    bool groundContact = backLeftCollider.GetGroundHit(out wheelHit);
    
    //edit wheel colliders
    WheelFrictionCurve frontLectFriction = frontLeftCollider.sidewaysFriction;
    frontLeftCollider.sidewaysFriction = frontLectFriction;

    WheelFrictionCurve frontRightFriction = frontRightCollider.sidewaysFriction;
    frontRightCollider.sidewaysFriction = frontRightFriction;
    
    WheelFrictionCurve backLectFriction = backLeftCollider.sidewaysFriction;
    backLectFriction.stiffness = driftStiffness;
    backLeftCollider.sidewaysFriction = backLectFriction;

    WheelFrictionCurve backRightFriction = backRightCollider.sidewaysFriction;
    backRightFriction.stiffness = driftStiffness;
    backRightCollider.sidewaysFriction = backRightFriction;

    float motorTorque = (groundContact) ? (motorForce*accelerateInput) : 0f;
    float steeringAngle = (maxSteerAngle*horizontalInput);

    backLeftCollider.motorTorque = motorTorque;
    backRightCollider.motorTorque = motorTorque;

    frontLeftCollider.steerAngle = steeringAngle;
    frontRightCollider.steerAngle = steeringAngle;

    waterElementController.SetFixedGravity((constantGravity || groundContact));

    //apply torque
    if(!groundContact){
        carRigidbody.AddTorque(
            transform.right*verticalInput*frontTorqueSpeed, ForceMode.Acceleration
        );

        if(rollInput != 0){
            carRigidbody.AddTorque(
                -transform.forward*horizontalInput*torqueSpeed, ForceMode.Acceleration
            );

        } else {
            carRigidbody.AddTorque(
                transform.up*horizontalInput*sideTorqueSpeed, ForceMode.Acceleration
            );
        }
    }

    //apply jump forces
    if(inputJump){
        if(groundContact){
            timeForDoubleJump = (Time.time+2f);

            AddJumpForce(transform.up);

        } else if(Time.time <= timeForDoubleJump){
            timeForDoubleJump = 0;

            carRigidbody.drag = 0f;
            carRigidbody.angularDrag = 0f;
            waterElementController.SetUnfixedGravity(true);

            Vector3 jumpDirection = transform.forward*verticalInput +
                transform.right*horizontalInput + transform.up*0.001f;
            AddJumpForce(jumpDirection);

            if(
                Mathf.Abs(verticalInput) > Mathf.Epsilon ||
                Mathf.Abs(horizontalInput) > Mathf.Epsilon
            ){
                Vector3 torqueDirection = transform.right*verticalInput -
                    transform.forward*horizontalInput;
                carRigidbody.AddTorque(
                    torqueDirection*doubleJumpForce, ForceMode.Impulse
                );
            }

            StartCoroutine(ResetJumpGravity());
        }
    }
    
    //apply boost
    ApplyBoost(boostInput);
    
    UpdateEngineAudio(verticalInput, boostInput);
}
"Tree Cutter"
  • 1 person
  • 3 weeks
  • private project - prototype
  • Unity Engine - C#
"Tree Cutter" is a proof-of-concept project, originally intended to be released on Christmas for mobile phones.
It is about a lumberjack who cuts trees and carries them home to his town, to sell them to villagers.

Game Features
  • Chop Trees based on your villagers needs
  • Carry and present the trees in your sales area
  • Sell your trees: the bigger the more expensive
  • Use your money to buy food
  • Drink of nearby rivers or lakes: The hungrier and thirstier, the slower you are
C# Code Example - Pulled Tree Forces and Rope Constraints
protected virtual void Update(){
    if(carryingTreeController != null){
        //calculate trees distance
        Vector3 treeDiff = transform.position -
            carryingTreeController.trunkRope.transform.position;
        float treeDistance = treeDiff.magnitude;

        if(treeDistance > 6f){
            //apply force to pull tree near to person
            carryingTreeController.transform.position +=
                treeDiff.normalized * (treeDistance-6f);
            if(carryingTreeController.body != null){
                carryingTreeController.body.AddForceAtPosition(
                    treeDiff, carryingTreeController.trunkRope.transform.position,
                    ForceMode.Acceleration
                );
            }

            if(Random.value < 0.1f){
                carryingTreeController.PlayPullSound();
            }
        }

        //update rope constraint positions
        rope.startPoint = rightHandTransform.position;
        rope.endPoint = carryingTreeController.trunkRope.transform.position;
        rope.minYPos = Mathf.Min(
            transform.position.y, carryingTreeController.trunkRope.transform.position.y
        );
    }
}

public void SetCarryTree(TreeController treeController){
    if(treeController != null && carryingTreeController == null){
        //set tree to carry
        if(treeController.body == null){
            StartCoroutine(treeController.AddRigidbody());
        }
        carryingTreeController = treeController;
        
        //add rope and set constraint positions
        rope = Instantiate(ropePrefab).GetComponent<Rope>();
        rope.transform.SetParent(transform, false);
        
        rope.startPoint = rightHandTransform.position;
        rope.endPoint = carryingTreeController.trunkRope.transform.position;
        rope.minYPos = Mathf.Min(
            transform.position.y, carryingTreeController.trunkRope.transform.position.y
        );

        carryingTreeController.trunkRope.SetActive(true);

        animator.SetBool("pull_tree", true);

    } else {
        //unset carrying tree, remove rope
        Destroy(rope.gameObject);

        carryingTreeController.trunkRope.SetActive(false);

        carryingTreeController = null;
        rope = null;

        animator.SetBool("pull_tree", false);
    }
}
Animator Example - Person Movement and Right Hand Overwriting

CV

Ubisoft Blue Byte GmbH, Mainz

Gameplay Programmer July 2022 - now
Junior Gameplay Programmer May 2021 - June 2022
In early 2021 I applied to a role in Ubisoft Mainz and started working on the Anno franchise in may. My first project was the Anno 1800 Console Edition which was released in March 2023.

MAJA Studios UG, Berlin

Founder/Programmer Oct 2016 - Feb 2021
MAJA Studios is the company I founded together with a close friend of mine. We began working on a browser-based engine in 2016 and in parallel worked on our first game Myràd.

The engine is browser-based, capable of server-client multiplayer networking using server-based JavaScript including the nodejs and socket.IO libraries. It uses the HTML5 Canvas element to render high resolution images and runs smoothly on all modern devices.

My roles in MAJA Studios included everything relevant for the studio including:
  • Writing and Testing the MAJA Engine
  • Setting up game servers running Linux CentOS 7
  • Creating the Story/Game Design/UI for Myràd
  • Being responsible for taxes and running the business
  • Providing support for the players through the engines support system and an external forum
After our first game Myràd, we decided to work on a second project and contact publishers that could help us with marketing. Due to personal reasons, we stopped this process in 2021 and decided to move our own ways.

Thüringer Energie AG, Erfurt

Junior IT Developer Jan 2015 - Oct 2016
After writing my bachelor thesis in this company, I was mainly responsible for the development of a web-based solution for photovoltaic monitoring.

I was also part of a team to plan and develop other in-house software solutions and was also responsible for the administration of multiple third-party software.

After I finished the photovoltaic monitoring project, I decided to move on to find further challenges in the games industry.

Bauhaus-Universität Weimar

Bachelor of Science Sep 2010 - Apr 2014
I got my bachelor’s degree in Weimar studying Computer Science and Media. My bachelor thesis was about visualizing time-based log data of photovoltaic systems which lead me to my first job.

The education included:
  • Mathematics: Calculus, Linear Algebra, Stochastics, Numerics, Discrete Mathematics
  • Programming: Formal languages and complexity, Algorightms & Data Structures, Introduction to Software Engineering, Programming Languages + Software Engineering, Information and Coding Theory, Parallel and distributed Systems, HCI, Media Security + Kryptology, Introduction to Computer Science, Databases, Web based technologies
  • Graphics: Visualization, Computer Graphics, Usability: Perceptual and Cognitive Foundations, Photogrammetric Computer Vision, Web Basics I
  • Audio: Audio Processing, Computer sounds - basics and practice
  • Others: Introduction to Media Economics, Media Law, Antagonists for visuospatial games, The Road to TRECT: A Competition on Web Search, Electrical engineering and systems theory, Three-dimensional web-interfaces - new approaches in visualization, interaction and animation
I also anticipated in the Games Master Class, a workshop hosted by the Fraunhofer Institute in Erfurt, which included talks and hosted projects with leading game developers from German game studios.

Contact Me

Jannis Harder - Gameplay Programmer