SnippetSample GameRoguelike Game

Creating a Roguelike Game


enemy-bullet-result

Steps

Add Enemies and Objects to the Scene

  1. Add an enemy object to the scene.
  2. Select the enemy object in the left explorer panel.
  3. Rename the object to โ€œEnemyโ€.
  4. Set the detailed parameters of the enemy (position, angle, size, etc.) in the right properties panel.
  5. Continuously adjust the values until you find the desired settings.
Create_enemy

If the Enemy Has Animations, Write the Animation Code

enemy-animation
let mixer; // Declare mixer globally
let moveAction;
 
function Start() {
    // Set up animation
    const object = WORLD.getObject("Enemy");
    
    mixer = new THREE.AnimationMixer(object);
    moveAction = mixer.clipAction(object.animations[1]);
 
    // Set animation speed 
    moveAction.setEffectiveTimeScale(1.7); // Movement animation speed
    
    moveAction.play();
}
 
function Update(dt) {
    // Update animation
    if (mixer) {
        mixer.update(dt);
    }
}

Specify the Spawn Location for Enemies and Write the Code

const enemys = [];
const object = WORLD.getObject("Enemy");
const minSpawnRadius = 20; 
const maxSpawnRadius = 100;
 
function SpawnRandomPos() {
    // Select a random angle
    const angle = Math.random() * 2 * Math.PI; // Random angle between 0 and 2ฯ€
    const radius = Math.random() * (maxSpawnRadius - minSpawnRadius) + minSpawnRadius; // Random radius between 20 and 100
 
    // Calculate x, z coordinates based on the angle and radius
    const x = PLAYER.position.x + radius * Math.cos(angle);
    const z = PLAYER.position.z + radius * Math.sin(angle);
    const y = 2; // Fixed y-coordinate
 
    const clone = THREEADDON.SkeletonUtils.clone(object);
 
    object.animations.forEach((item) => {
        clone.animations.push(item);
    });
    clone.position.set(x, y, z);
    WORLD.add(clone);
    const enemy = new Enemy(clone);
    
    enemys.push(enemy); 
}

Set the Time for Enemy Creation to Spawn at Regular Intervals

enemy-random-spawn
let spawnInterval = 300;
function CreateEnemy() {
    clearInterval(spawnInterval);
    spawnInterval = setInterval(() => {
        SpawnRandomPos();
    }, spawnInterval);
}

Write Code for Enemies to Move Towards the Player

enemy-move-to-player
function Move(dt) {
    // animation update
    if (mixer) { mixer.update(dt); }
 
    const direction = new THREE.Vector3(
        PLAYER.position.x - object.position.x,
        0,
        PLAYER.position.z - object.position.z
    );
 
    direction.normalize();
 
    // move
    object.position.add(direction.multiplyScalar(speed * dt));
    const lookAtPosition = new THREE.Vector3(
        PLAYER.position.x,
        object.position.y,
        PLAYER.position.z
    );
    object.lookAt(lookAtPosition);
}

Add Collision Logic to Prevent Overlapping Between Enemies

enemy-collide-to-enemy
function Move(dt) {
    // animation update
    if (mixer) { mixer.update(dt); }
 
    const direction = new THREE.Vector3(
        PLAYER.position.x - object.position.x,
        0,
        PLAYER.position.z - object.position.z
    );
 
    direction.normalize();
    
    // maintain distance from other enemies
    const separationForce = new THREE.Vector3();
    for (let enemy of enemys) {
        if (enemy !== this) {
            const distance = this._object.position.distanceTo(enemy._object
                .position);
            if (distance < this._radius + enemy._radius) { // Collision avoidance distance
                const diff = new THREE.Vector3().subVectors(this._object.position,
                    enemy._object.position);
                diff.normalize();
                separationForce.add(diff); // Add force to push them away from each other
            }
        }
    }
 
    direction.add(separationForce);
    direction.normalize();
    
    // move
    object.position.add(direction.multiplyScalar(speed * dt));
    const lookAtPosition = new THREE.Vector3(
        PLAYER.position.x,
        object.position.y,
        PLAYER.position.z
    );
    object.lookAt(lookAtPosition);
}

Add Bullets

  • Add bullet objects to the scene.
    Create_enemy

Implement bullet generation at regular intervals and the movement of generated bullets.

  • Implement the creation of bullet objects at regular intervals.
  • Implement bullet movement in the direction the player is facing.
class BulletSystem {
    constructor() {
        this._bullets = [];
        this._object = WORLD.getObject("bullet");
        this._bulletSpeed = 50;
        this._bulletRange = 20000;
        this._shootingInterval = 1000;
    }
 
    CreateBullet() {
        const bullet = this._object.clone();
 
        const direction = new THREE.Vector3(0, 0, 1); // front
 
        if (!bullet) return;
        bullet.position.set(PLAYER.position.x, PLAYER.position.y + 2, PLAYER.position.z);
        bullet.direction = direction.applyEuler(PLAYER.rotation).normalize();
        bullet.lookAt(bullet.position.clone().add(bullet.direction));
        this._bullets.push({ object: bullet, direction: bullet.direction });
        WORLD.add(bullet);
    }
 
    MoveBullet(dt) {
        let movementVector;
        const toRemove = []; // Array to store the index of the enemy to be deleted
 
        for (let i = this._bullets.length - 1; i >= 0; i--) {
            const bullet = this._bullets[i];
            const bulletPosition = bullet.object.position;
 
            movementVector = bullet.direction.clone().multiplyScalar(this._bulletSpeed * dt);
 
            bullet.object.position.set(
                bulletPosition.x + movementVector.x,
                bulletPosition.y + movementVector.y,
                bulletPosition.z + movementVector.z,
            );
 
            if (bullet.object.position.distanceToSquared(PLAYER.position) > 10000) {
                WORLD.remove(bullet.object);
                this._bullets.splice(i, 1);
                continue;
            }
        }
    }
}
GLOBAL.bulletSystem = new BulletSystem();
 
let shotInterval;
 
function Fire() {
    clearInterval(hotInterval);
 
    shotInterval = setInterval(() => {
        GLOBAL.bulletSystem.CreateBullet();
    }, 500);
 
}
 
const startButton = GUI.getObject("StartButton");
 
function Start() {
    startButton.onClick(function() {
        Fire();
        startButton.hide();
    })
}
 
function Update(dt) {
    if (!GLOBAL.isGameStart) return;
 
    GLOBAL.bulletSystem.MoveBullet(dt);
}

Implement Bullet and Enemy Collision

  • Implement collision logic with enemies and subsequent actions.
collide-enemy
Collide(bullet, enemy, toRemove, num) {
        if (bullet.object.position.distanceToSquared(enemy._object.position) < 5) {
 
            if (enemy._isInvincibility === true) return;
 
            enemy._isInvincibility = true;
 
            if (enemy._alive) {
                enemy._alive = false;
                const enemyObject = enemy._object;
 
                enemy._moveAction.stop();
                enemy._deathAction.play();
                enemy._deathAction.setLoop(THREE.LoopOnce);
 
                // Save the index of the enemy to be deleted
 
                setTimeout(() => {
                    toRemove.push(num);
                    enemyObject.position.set(0, -100, 0);
                    WORLD.remove(enemyObject);
                }, 1000);
 
            }
 
        }
    }
 
    Remove(toRemove) {
        // Delete enemies at once
        for (let i = toRemove.length - 1; i >= 0; i--) {
            GLOBAL.enemys.splice(toRemove[i], 1);
            toRemove.splice(i,1);
        }
    }

Complete Code Integration

Hereโ€™s the complete code with all components integrated, including the bullet and enemy collision.

enemy-bullet-result
class Enemy {
    constructor(object) {
        this._object = object;
        this._speed = 10;
        this._hp = 100;
 
        // Collision avoidance distance
        this._radius = 1;
 
        // Animation
        this._mixer = new THREE.AnimationMixer(this._object);
        this._moveAction = this._mixer.clipAction(this._object.animations[1]);
 
        // Animation speed 
        this._moveAction.setEffectiveTimeScale(1.7); // Movement animation speed
 
        this.Init();
    }
 
    Init() {
        // Start the move action
        this._moveAction.play();
    }
 
    Move(dt) {
        if (this._hp < 0) return;
 
        // Animation update
        if (this._mixer) { this._mixer.update(dt); }
 
        const direction = new THREE.Vector3(
            PLAYER.position.x - this._object.position.x,
            0,
            PLAYER.position.z - this._object.position.z
        );
 
        direction.normalize();
 
        // Maintain distance from other enemies
        const separationForce = new THREE.Vector3();
        for (let enemy of enemys) {
            if (enemy !== this) {
                const distance = this._object.position.distanceTo(enemy._object.position);
                if (distance < this._radius + enemy._radius) { // Collision avoidance distance
                    const diff = new THREE.Vector3().subVectors(this._object.position, enemy._object.position);
                    diff.normalize();
                    separationForce.add(diff); // Add force to move apart
                }
            }
        }
 
        direction.add(separationForce);
        direction.normalize();
 
        // Move the enemy
        this._object.position.add(direction.multiplyScalar(this._speed * dt));
        const lookAtPosition = new THREE.Vector3(
            PLAYER.position.x,
            this._object.position.y,
            PLAYER.position.z
        );
        this._object.lookAt(lookAtPosition);
    }
}
 
class BulletSystem {
    constructor() {
        this._bullets = [];
        this._object = WORLD.getObject("bullet");
        this._bulletSpeed = 50;
        this._bulletRange = 20000;
        this._dmg = 10;
    }
 
    CreateBullet() {
        const bullet = this._object.clone();
        const direction = new THREE.Vector3(0, 0, 1); // Front direction
 
        if (!bullet) return;
        bullet.position.set(PLAYER.position.x, PLAYER.position.y + 2, PLAYER.position.z);
        bullet.direction = direction.applyEuler(PLAYER.rotation).normalize();
        bullet.lookAt(bullet.position.clone().add(bullet.direction));
        this._bullets.push({ object: bullet, direction: bullet.direction });
        WORLD.add(bullet);
    }
 
    MoveBullet(dt) {
        let movementVector;
        const toRemove = []; // Array to store indices of bullets to remove
 
        for (let i = this._bullets.length - 1; i >= 0; i--) {
            const bullet = this._bullets[i];
            const bulletPosition = bullet.object.position;
 
            movementVector = bullet.direction.clone().multiplyScalar(this._bulletSpeed * dt);
            bullet.object.position.set(
                bulletPosition.x + movementVector.x,
                bulletPosition.y + movementVector.y,
                bulletPosition.z + movementVector.z,
            );
 
            // Collision check
            for (let j = 0; j < GLOBAL.enemys.length; j++) {
                this.Collide(bullet, GLOBAL.enemys[j], toRemove, j);
            }
 
            if (bullet.object.position.distanceToSquared(PLAYER.position) > 10000) {
                WORLD.remove(bullet.object);
                this._bullets.splice(i, 1);
                continue;
            }
        }
        this.Remove(toRemove);
    }
 
    Collide(bullet, enemy, toRemove, num) {
        if (bullet.object.position.distanceToSquared(enemy._object.position) < 5) {
            if (enemy._isInvincibility === true) return;
 
            enemy._isInvincibility = true;
 
            if (enemy._alive) {
                enemy._alive = false;
                const enemyObject = enemy._object;
 
                enemy._moveAction.stop();
                enemy._deathAction.play();
                enemy._deathAction.setLoop(THREE.LoopOnce);
 
                // Store the index of the enemy to be removed
                setTimeout(() => {
                    toRemove.push(num);
                    enemyObject.position.set(0, -100, 0);
                    WORLD.remove(enemyObject);
                }, 1000);
            }
        }
    }
 
    Remove(toRemove) {
        // Remove enemies at once
        for (let i = toRemove.length - 1; i >= 0; i--) {
            GLOBAL.enemys.splice(toRemove[i], 1);
        }
    }
}
 
GLOBAL.bulletSystem = new BulletSystem();
 
let shotInterval;
 
function Fire() {
    clearInterval(shotInterval);
 
    shotInterval = setInterval(() => {
        GLOBAL.bulletSystem.CreateBullet();
    }, 500);
}
 
const startButton = GUI.getObject("StartButton");
 
function Start() {
    startButton.onClick(function() {
        CreateEnemy();
        Fire();
        startButton.hide();
    });
}
 
function Update(dt) {
    for (let i = 0; i < enemys.length; i++) {
        enemys[i].Move(dt);
    }
    GLOBAL.bulletSystem.MoveBullet(dt);
}
 

Summary

  • Enemy Class: Handles enemy movement towards the player, including separation logic to avoid overlapping with other enemies.

  • BulletSystem Class: Manages bullet creation, movement, and collision detection with enemies. Bullets are removed from the scene if they go out of range or hit an enemy.

  • Start and Update Functions: Manage the gameโ€™s start process and update the state of enemies and bullets each frame.

This structure allows you to create a simple survivor-like game where enemies spawn, move towards the player, and bullets can collide with enemies.