I write visual, interactive essays about code, creative coding, and software engineering. Each post aims to explain concepts from the ground up with hands-on demos.

← Back

Building a Multiplayer Wizard Duel Game

7 min readgame-dev

In this post, I'll walk you through the creation of my simple game Harry Potter 2026 which is a multiplayer wizard duel game where players can battle each other with some iconic spells (included avada kedavra) in the Hogwarts (maybe only the name looks like Hogwarts) courtyard. We'll explore a bit about the game engine, spell physics, how to aim to opponents, and how it all comes together.

P.S.

This is my first interactive blog post. I'm not sure how everything works, I'm still learning. I'll try my best to explain things in my own words.

Why?

Maybe some of you are wondering why I'm writing about harry potter 2027 game. Well, I'm just a fan of the Harry Potter series (I've read the books as well no need to ask me) and new tv series is coming soon. Also, I am learning three.js and socket.io and I wanted to make some things that I can share.

The Game

Players choose from four characters (Harry, Hermione, Draco, or Voldemort). They battle in real-time using spells like Stupefy (which gives some damage), Petrificus Totalus (which paralyzes the opponent), Protego (which creates a shield), and the Avada Kedavra (which cause instant death).


Tech Stack

I used some technologies to build this game:

  • Three.js - 3D rendering, camera controls, and scene management
  • Socket.io - Real-time bidirectional communication for multiplayer
  • Next.js - Server-side rendering and API routes
import * as THREE from 'three';
import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls.js';
import { Socket } from 'socket.io-client';

export class GameEngine {
    private scene: THREE.Scene;
    private camera: THREE.PerspectiveCamera;
    private renderer: THREE.WebGLRenderer;
    private controls: PointerLockControls | null = null;
    private socket: Socket;
    // ... and 1400+ lines of unnecessary game logic
}

Procedural Character Meshes

Instead of loading external 3D models (I didn't want to use ready models), I built characters procedurally using Three.js primitives. Each character is assembled from cones, spheres, cylinders, and toruses to give specific appearance to each character like Harry's glasses or Voldemort's snake-like nose slits.

Harry Potter·Black messy hair · Round glasses · Lightning scar

Click the character buttons and drag to rotate. (I know when you turn the character they become bald. I'll fix it later.)

Here's how the character mesh is built:

private createPlayerMesh(character: string): THREE.Group {
    const group = new THREE.Group();
    const robeColor = CHARACTER_COLORS[character] || 0x740001;

    // Robe/body (cone shape)
    const robeGeom = new THREE.ConeGeometry(0.5, 1.6, 8);
    const robeMat = new THREE.MeshStandardMaterial({ color: robeColor });
    const robe = new THREE.Mesh(robeGeom, robeMat);
    robe.position.y = 0.8;
    group.add(robe);

    // Head with skin tone
    const skinColor = character === 'voldemort' ? 0xd4d4d4 : 0xffe0bd;
    const headGeom = new THREE.SphereGeometry(0.22, 16, 16);
    const headMat = new THREE.MeshStandardMaterial({ color: skinColor });
    const head = new THREE.Mesh(headGeom, headMat);
    head.position.y = 1.95;
    group.add(head);

    // Character-specific features...
    if (character === 'harry') {
        // Round glasses
        const glassMat = new THREE.MeshBasicMaterial({ color: 0x333333 });
        const leftRim = new THREE.Mesh(
            new THREE.TorusGeometry(0.06, 0.01, 8, 16),
            glassMat
        );
        leftRim.position.set(-0.08, 1.97, 0.18);
        group.add(leftRim);
        // Lightning scar, hair, etc...
    }

    return group;
}

I know it's not the best way to do it.


Spell Physics with Bezier Curves

At first, I was going to use straight lines for spells, but then I thought it would be a lot more magical to use Bezier curves.

Loading spell demo...

Click anywhere to cast a spell. Drag the wizards to reposition them.

The spell trajectory is calculated using a control point above the midpoint:

private createCurvySpellEffect(data: any) {
    const startPos = new THREE.Vector3(data.casterPosition.x, 1.3, data.casterPosition.z);
    const endPos = new THREE.Vector3(data.targetPosition.x, 1.3, data.targetPosition.z);

    // Calculate curve control point
    const midPoint = new THREE.Vector3()
        .addVectors(startPos, endPos)
        .multiplyScalar(0.5);
    const distance = startPos.distanceTo(endPos);
    const curveHeight = distance * 0.15 + Math.random() * 0.5;

    const controlPoint = new THREE.Vector3(
        midPoint.x,
        midPoint.y + curveHeight,
        midPoint.z
    );

    // Create Bezier curve
    const curve = new THREE.QuadraticBezierCurve3(startPos, controlPoint, endPos);

    // Animate along curve
    const animateSpell = () => {
        const t = elapsed / duration;
        const pos = curve.getPoint(t);
        spellMesh.position.copy(pos);
        // Add particle trails...
    };
}

The Bezier formula interpolates between three points:

B(t) = (1-t)² × P₀ + 2(1-t)t × P₁ + t² × P₂

Where P₀ is the start, P₁ is the control point (above center), and P₂ is the target.


Raycasting for Targeting

To detect which player you're aiming at, I use raycasting. I didn't know about this before, but it's a very useful tool for detecting collisions and intersections.

I can simple explain that raycasting here is shooting an invisible ray from the camera and checking for intersections with player meshes. Counter Strike is using this method to detect where the bullet is going to hit.

Camera (ray origin)
No target

Move your mouse to aim the ray at different targets.

Here's the targeting implementation:

private checkTargeting() {
    const raycaster = new THREE.Raycaster();
    // Cast ray from center of screen (crosshair)
    raycaster.setFromCamera(new THREE.Vector2(0, 0), this.camera);

    // Get all player meshes except self
    const playerMeshes = Array.from(this.players.values());
    const intersects = raycaster.intersectObjects(playerMeshes, true);

    if (intersects.length > 0) {
        // Walk up the hierarchy to find the player group
        let obj = intersects[0].object;
        while (obj.parent && !obj.userData.playerId) {
            obj = obj.parent as THREE.Object3D;
        }

        if (obj.userData.playerId) {
            this.isAimingAtTarget = obj.userData.playerId;
            this.onTargetChange?.(true);
            return;
        }
    }

    this.isAimingAtTarget = null;
    this.onTargetChange?.(false);
}

The raycaster checks for intersections recursively (true parameter) since each character mesh is made of multiple sub-meshes.


Multiplayer Architecture

It's a simple architecture, but it works.

// Server-side spell handling
socket.on('spell:cast', (data) => {
    const caster = players.get(socket.id);
    const target = players.get(data.targetId);

    // Check if target has Protego active
    if (target.protego.active && target.protego.endTime > Date.now()) {
        // Perfect timing reflection (within 300ms)
        const protegoAge = Date.now() - target.protego.startTime;
        if (protegoAge < 300) {
            // Reflect spell back!
            applySpellEffect(io, data.spellType, data.targetId, socket.id);
            return;
        }
        // Normal block
        io.emit('spell:blocked', { casterId: socket.id, targetId: data.targetId });
        return;
    }

    // Apply spell effect
    applySpellEffect(io, data.spellType, socket.id, data.targetId);
});

Note: The Protego timing window creates a risk/reward mechanic. If you activate the spell too early, it wears off. If you activate it too late, you get hit. But if you activate it at the perfect timing, the spell reflects back!


Mobile Support (Not fully working)

The game detects mobile devices and switches to touch controls:

  • Left side of screen - Virtual joystick for movement
  • Right side of screen - Touch drag for camera rotation
  • HUD buttons - Spell casting and actions
private setupMobileControls() {
    this.isControlsLocked = true; // No pointer lock on mobile

    canvas.addEventListener('touchstart', (e) => {
        const screenWidth = window.innerWidth;
        for (const touch of e.changedTouches) {
            if (touch.clientX < screenWidth * 0.4) {
                // Left side - joystick
                this.touchJoystickActive = true;
                this.touchJoystickStart = { x: touch.clientX, y: touch.clientY };
            } else {
                // Right side - look
                this.touchLookActive = true;
            }
        }
    });
}

Conclusion

I started my journey with game development. There was a game (there is still the game) called "Metin2" which is a MMORPG game. I started to learn game development with this game. But I stopped and switch to web development. So, I forgot how hard actually game development is. I wanted to learn more about three.js by practicing. So, I thought it would be a great opportunity to make a game. I started to make a game called "Harry Potter". I used three.js for the game engine and socket.io for the multiplayer things.

The full source code and the link will be available soon here. If I don't please hit me on below links. Try dueling your friends!