Template: Purple Dimension
This template walks you through building a fully working custom dimension from scratch. By the end, you will have a Purple Dimension with purple grass, purple stone, thick purple fog, floating terrain, and a portal to get there and back.
What you will build
Section titled “What you will build”- PurpleDimension class (dimension ID 3) with purple fog and permanent twilight
- PurpleChunkSource that generates floating islands out of purple stone
- Two custom tiles: Purple Stone and Purple Grass
- A portal block that teleports players between the Overworld and the Purple Dimension
- All the registration and wiring to make it work in-game
Systems you will learn
Section titled “Systems you will learn”- Subclassing
Dimensionand overriding sky, fog, and spawn behavior - Writing a
ChunkSourcewith noise-based terrain - Registering custom tiles (blocks) with materials and properties
- Building portal logic with
HalfTransparentTileandServerPlayertick handling - Hooking everything into the factory methods and static constructors
If any of these systems are new to you, the reference pages go deeper:
- Custom Dimensions covers every virtual method on
Dimension - Adding Blocks covers the full
Tileregistration system - Fog & Sky covers the rendering pipeline for fog colors
- Custom Materials covers
Materialbehavior flags - Custom World Generation covers noise and feature placement
Step 1: Create the Purple Stone tile
Section titled “Step 1: Create the Purple Stone tile”Start with the blocks, since the chunk source and dimension will reference them. Purple Stone is the base terrain block for the dimension, like how the Aether uses holystone.
PurpleStoneTile.h
#pragma once#include "Tile.h"
class PurpleStoneTile : public Tile{public: PurpleStoneTile(int id);};PurpleStoneTile.cpp
#include "stdafx.h"#include "PurpleStoneTile.h"
PurpleStoneTile::PurpleStoneTile(int id) : Tile(id, Material::stone){}That is it for the class. All the interesting stuff happens during registration. Open Tile.h and add:
// Forward declarationclass PurpleStoneTile;
// Inside the Tile class, with the other static pointersstatic PurpleStoneTile *purpleStoneTile;static const int purpleStone_Id = 200;Then in Tile.cpp, add the static definition and register it inside Tile::staticCtor():
PurpleStoneTile *Tile::purpleStoneTile = NULL;
// Inside Tile::staticCtor():Tile::purpleStoneTile = (PurpleStoneTile *)(new PurpleStoneTile(200)) ->setDestroyTime(1.5f) ->setExplodeable(10) ->setSoundType(Tile::SOUND_STONE) ->setTextureName(L"purpleStone") ->setDescriptionId(IDS_TILE_PURPLE_STONE) ->setBaseItemTypeAndMaterial(Item::eBaseItemType_block, Item::eMaterial_stone);Pick IDs that are not already taken. 200 and 201 are used here as examples. Check your project for conflicts. See Getting Started for finding available IDs.
Step 2: Create the Purple Grass tile
Section titled “Step 2: Create the Purple Grass tile”Purple Grass goes on top of purple stone, like how regular grass sits on dirt. Same pattern as above.
PurpleGrassTile.h
#pragma once#include "Tile.h"
class PurpleGrassTile : public Tile{public: PurpleGrassTile(int id);};PurpleGrassTile.cpp
#include "stdafx.h"#include "PurpleGrassTile.h"
PurpleGrassTile::PurpleGrassTile(int id) : Tile(id, Material::dirt){}Register it the same way. In Tile.h:
class PurpleGrassTile;
static PurpleGrassTile *purpleGrassTile;static const int purpleGrass_Id = 201;In Tile.cpp:
PurpleGrassTile *Tile::purpleGrassTile = NULL;
// Inside Tile::staticCtor():Tile::purpleGrassTile = (PurpleGrassTile *)(new PurpleGrassTile(201)) ->setDestroyTime(0.6f) ->setExplodeable(3) ->setSoundType(Tile::SOUND_GRASS) ->setTextureName(L"purpleGrass") ->setDescriptionId(IDS_TILE_PURPLE_GRASS) ->setBaseItemTypeAndMaterial(Item::eBaseItemType_block, Item::eMaterial_dirt);Both tiles will need textures. The setTextureName() call tells the game which texture to look up. You will need to add purpleStone and purpleGrass entries to the texture atlas. See Block Textures for how that works.
Step 3: Create the PurpleBiome
Section titled “Step 3: Create the PurpleBiome”The dimension needs a biome to control surface blocks, mob spawning, and decoration. For a simple first pass, use a custom biome with no mobs and basic trees.
PurpleBiome.h
#pragma once#include "Biome.h"
class PurpleBiome : public Biome{public: PurpleBiome(int id); virtual int getGrassColor(); virtual int getFolageColor(); virtual int getSkyColor(float temp);};PurpleBiome.cpp
#include "stdafx.h"#include "PurpleBiome.h"#include "BiomeDecorator.h"
PurpleBiome::PurpleBiome(int id) : Biome(id){ // Clear default mob spawning (empty dimension for now) enemies.clear(); friendlies.clear(); friendlies_chicken.clear(); friendlies_wolf.clear(); waterFriendlies.clear();
// Surface blocks topMaterial = (byte) Tile::purpleGrass_Id; material = (byte) Tile::purpleStone_Id;
// Keep the default BiomeDecorator for basic trees and flowers. // You can swap in a custom one later.}
int PurpleBiome::getGrassColor(){ return 0x9B30FF; // purple tint for grass overlay}
int PurpleBiome::getFolageColor(){ return 0x7B2FBE; // purple tint for leaves}
int PurpleBiome::getSkyColor(float temp){ return 0x6A0DAD; // deep purple sky}Register the biome in Biome.h:
static Biome *purple;And in Biome.cpp:
Biome *Biome::purple = NULL;
// In the biome initialization block:Biome::purple = (new PurpleBiome(24)) ->setColor(0x9B30FF) ->setName(L"Purple") ->setNoRain() ->setTemperatureAndDownfall(0.5f, 0.0f);Use a biome ID that does not conflict with existing ones. The Aether uses 23, so 24 is a safe pick.
Step 4: Create the PurpleChunkSource
Section titled “Step 4: Create the PurpleChunkSource”This is the terrain generator. It creates floating islands out of purple stone, then paints the top layer with purple grass. The approach is the same one the Aether uses (Perlin noise for island shapes), but simplified.
PurpleChunkSource.h
#pragma once#include "ChunkSource.h"
class Level;class Random;class PerlinNoise;class LevelChunk;class ProgressListener;
class PurpleChunkSource : public ChunkSource{public: PurpleChunkSource(Level *level, __int64 seed); ~PurpleChunkSource();
virtual bool hasChunk(int x, int y); virtual LevelChunk *getChunk(int x, int z); virtual LevelChunk *create(int x, int z); virtual void lightChunk(LevelChunk *lc); virtual void postProcess(ChunkSource *parent, int x, int z); virtual bool save(bool force, ProgressListener *progressListener); virtual bool tick(); virtual bool shouldSave(); virtual wstring gatherStats(); virtual vector<Biome::MobSpawnerData *> *getMobsAt( MobCategory *mobCategory, int x, int y, int z); virtual TilePos *findNearestMapFeature( Level *level, const wstring& featureName, int x, int y, int z);
private: void prepareHeights(int xOffs, int zOffs, byteArray& blocks); void buildSurfaces(int xOffs, int zOffs, byteArray& blocks); double *getHeights(double *buffer, int x, int y, int z, int xSize, int ySize, int zSize);
Level *level; Random *random; Random *pprandom; PerlinNoise *lperlinNoise1; PerlinNoise *lperlinNoise2; PerlinNoise *perlinNoise1; PerlinNoise *islandNoise; PerlinNoise *depthNoise;};PurpleChunkSource.cpp
#include "stdafx.h"#include "PurpleChunkSource.h"#include "net.minecraft.world.level.h"#include "net.minecraft.world.level.biome.h"#include "net.minecraft.world.level.chunk.h"#include "PerlinNoise.h"#include "Random.h"#include "Mth.h"
static const int CHUNK_WIDTH = 4;static const int CHUNK_HEIGHT = 8;
PurpleChunkSource::PurpleChunkSource(Level *level, __int64 seed){ m_XZSize = level->getLevelData()->getXZSize(); this->level = level;
random = new Random(seed); pprandom = new Random(seed);
lperlinNoise1 = new PerlinNoise(random, 16); lperlinNoise2 = new PerlinNoise(random, 16); perlinNoise1 = new PerlinNoise(random, 8); islandNoise = new PerlinNoise(random, 4); depthNoise = new PerlinNoise(random, 16);}
PurpleChunkSource::~PurpleChunkSource(){ delete random; delete pprandom; delete lperlinNoise1; delete lperlinNoise2; delete perlinNoise1; delete islandNoise; delete depthNoise;}The getChunk method
Section titled “The getChunk method”This is the main entry point. It allocates a block array, runs prepareHeights to fill in the terrain shape, then runs buildSurfaces to paint the top layer.
LevelChunk *PurpleChunkSource::getChunk(int xOffs, int zOffs){ random->setSeed(xOffs * 341873128712l + zOffs * 132897987541l);
BiomeArray biomes; unsigned int blocksSize = Level::genDepth * 16 * 16; byte *tileData = (byte *)XPhysicalAlloc( blocksSize, MAXULONG_PTR, 4096, PAGE_READWRITE); XMemSet128(tileData, 0, blocksSize); byteArray blocks = byteArray(tileData, blocksSize);
level->getBiomeSource()->getBiomeBlock( biomes, xOffs * 16, zOffs * 16, 16, 16, true);
prepareHeights(xOffs, zOffs, blocks); buildSurfaces(xOffs, zOffs, blocks);
LevelChunk *levelChunk = new LevelChunk(level, blocks, xOffs, zOffs); XPhysicalFree(tileData);
delete biomes.data; return levelChunk;}prepareHeights: the noise sampling
Section titled “prepareHeights: the noise sampling”This method samples noise at intervals and fills blocks where the noise value is positive. The noise controls the island shapes. Where the combined noise is above zero, place purple stone. Where it is zero or below, leave air.
void PurpleChunkSource::prepareHeights(int xOffs, int zOffs, byteArray& blocks){ int xSize = CHUNK_WIDTH + 1; int ySize = Level::genDepth / CHUNK_HEIGHT + 1; int zSize = CHUNK_WIDTH + 1;
double *heights = getHeights( NULL, xOffs * CHUNK_WIDTH, 0, zOffs * CHUNK_WIDTH, xSize, ySize, zSize);
for (int cx = 0; cx < CHUNK_WIDTH; cx++) { for (int cz = 0; cz < CHUNK_WIDTH; cz++) { for (int cy = 0; cy < ySize - 1; cy++) { // Corners of this noise cell double v000 = heights[((cx + 0) * zSize + (cz + 0)) * ySize + (cy + 0)]; double v001 = heights[((cx + 0) * zSize + (cz + 0)) * ySize + (cy + 1)]; double v100 = heights[((cx + 1) * zSize + (cz + 0)) * ySize + (cy + 0)]; double v101 = heights[((cx + 1) * zSize + (cz + 0)) * ySize + (cy + 1)]; double v010 = heights[((cx + 0) * zSize + (cz + 1)) * ySize + (cy + 0)]; double v011 = heights[((cx + 0) * zSize + (cz + 1)) * ySize + (cy + 1)]; double v110 = heights[((cx + 1) * zSize + (cz + 1)) * ySize + (cy + 0)]; double v111 = heights[((cx + 1) * zSize + (cz + 1)) * ySize + (cy + 1)];
int blockStepY = CHUNK_HEIGHT; int blockStepXZ = 16 / CHUNK_WIDTH;
// Trilinear interpolation within this cell for (int dy = 0; dy < blockStepY; dy++) { double ty = (double)dy / blockStepY; double d00 = v000 + (v001 - v000) * ty; double d10 = v100 + (v101 - v100) * ty; double d01 = v010 + (v011 - v010) * ty; double d11 = v110 + (v111 - v110) * ty;
for (int dx = 0; dx < blockStepXZ; dx++) { double tx = (double)dx / blockStepXZ; double dz0 = d00 + (d10 - d00) * tx; double dz1 = d01 + (d11 - d01) * tx;
for (int dz = 0; dz < blockStepXZ; dz++) { double tz = (double)dz / blockStepXZ; double val = dz0 + (dz1 - dz0) * tz;
int bx = cx * blockStepXZ + dx; int by = cy * blockStepY + dy; int bz = cz * blockStepXZ + dz; int offs = (bx * 16 + bz) * Level::genDepth + by;
int tileId = 0; if (val > 0) { tileId = Tile::purpleStone_Id; } blocks[offs] = (byte)tileId; } } } } } }
delete[] heights;}getHeights: the noise pipeline
Section titled “getHeights: the noise pipeline”This is where the island shapes come from. It combines multiple noise layers to produce floating terrain. The island noise creates scattered clusters, and the slide functions at the top and bottom force terrain to zero near the world ceiling and floor.
double *PurpleChunkSource::getHeights(double *buffer, int x, int y, int z, int xSize, int ySize, int zSize){ int total = xSize * zSize * ySize; if (buffer == NULL) buffer = new double[total];
// Sample noise fields double *noise1 = lperlinNoise1->getRegion( NULL, x, y, z, xSize, ySize, zSize, 684.412, 684.412, 684.412); double *noise2 = lperlinNoise2->getRegion( NULL, x, y, z, xSize, ySize, zSize, 684.412, 684.412, 684.412); double *blendNoise = perlinNoise1->getRegion( NULL, x, y, z, xSize, ySize, zSize, 684.412 / 80.0, 684.412 / 160.0, 684.412 / 80.0); double *islands = islandNoise->getRegion( NULL, x, z, xSize, zSize, 1.121, 1.121, 0); double *depth = depthNoise->getRegion( NULL, x, z, xSize, zSize, 200.0, 200.0, 0);
int idx = 0; int idx2d = 0;
for (int xx = 0; xx < xSize; xx++) { for (int zz = 0; zz < zSize; zz++) { // Island threshold: only generate terrain where this is high enough double islandVal = (islands[idx2d] + 256) / 512.0; double islandThreshold = islandVal * 100 - 60;
double depthVal = depth[idx2d] / 8000.0; if (depthVal < 0) depthVal = -depthVal * 0.3; depthVal = depthVal * 3 - 2; if (depthVal < 0) depthVal = depthVal / 2; if (depthVal > 1) depthVal = 1; depthVal = depthVal / 8;
idx2d++;
for (int yy = 0; yy < ySize; yy++) { double n1 = noise1[idx] / 512.0; double n2 = noise2[idx] / 512.0; double blend = (blendNoise[idx] / 10.0 + 1) / 2.0;
double val; if (blend < 0) val = n1; else if (blend > 1) val = n2; else val = n1 + (n2 - n1) * blend;
// Vertical center bias: terrain clusters around y=64 double centerBias = ((double)yy - (ySize / 2.0 + depthVal)) * 12.0; if (centerBias > 0) centerBias *= 1.5; val = val - centerBias;
// Island threshold masking if (islandThreshold < 0) val = val + islandThreshold;
// Top slide: force zero near ceiling if (yy > ySize - 4) { double slide = (yy - (ySize - 4)) / 3.0; val = val * (1 - slide) + -3000 * slide; } // Bottom slide: force zero near floor if (yy < 8) { double slide = (8 - yy) / 7.0; val = val * (1 - slide) + -30 * slide; }
buffer[idx] = val; idx++; } } }
delete[] noise1; delete[] noise2; delete[] blendNoise; delete[] islands; delete[] depth;
return buffer;}buildSurfaces: painting the top layer
Section titled “buildSurfaces: painting the top layer”Walk each column from top to bottom. When you hit the first purple stone block, replace it with purple grass. The next few blocks below become purple stone (they already are, so nothing to do). This is the same approach the Aether uses with its buildSurfaces method.
void PurpleChunkSource::buildSurfaces(int xOffs, int zOffs, byteArray& blocks){ for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { int run = -1; int runDepth = 3;
for (int y = Level::genDepthMinusOne; y >= 0; y--) { int offs = (x * 16 + z) * Level::genDepth + y; int old = blocks[offs];
if (old == 0) { // Air resets the run run = -1; } else if (old == Tile::purpleStone_Id) { if (run == -1) { // First stone from the top: replace with grass run = runDepth; blocks[offs] = (byte)Tile::purpleGrass_Id; } else if (run > 0) { // Below the grass: keep as purple stone run--; } } } } }}Boilerplate methods
Section titled “Boilerplate methods”These are the same for basically every custom chunk source:
LevelChunk *PurpleChunkSource::create(int x, int z){ return getChunk(x, z);}
void PurpleChunkSource::lightChunk(LevelChunk *lc){ lc->recalcHeightmap();}
void PurpleChunkSource::postProcess(ChunkSource *parent, int xt, int zt){ HeavyTile::instaFall = true; int xo = xt * 16; int zo = zt * 16;
pprandom->setSeed(level->getSeed()); __int64 xScale = pprandom->nextLong() / 2 * 2 + 1; __int64 zScale = pprandom->nextLong() / 2 * 2 + 1; pprandom->setSeed(((xt * xScale) + (zt * zScale)) ^ level->getSeed());
Biome *biome = level->getBiome(xo + 16, zo + 16); biome->decorate(level, pprandom, xo, zo);
HeavyTile::instaFall = false;}
bool PurpleChunkSource::hasChunk(int x, int y) { return true; }bool PurpleChunkSource::save(bool force, ProgressListener *p) { return true; }bool PurpleChunkSource::tick() { return false; }bool PurpleChunkSource::shouldSave() { return true; }wstring PurpleChunkSource::gatherStats() { return L"PurpleChunkSource"; }
TilePos *PurpleChunkSource::findNearestMapFeature( Level *level, const wstring& featureName, int x, int y, int z){ return NULL;}
vector<Biome::MobSpawnerData *> *PurpleChunkSource::getMobsAt( MobCategory *mobCategory, int x, int y, int z){ Biome *biome = level->getBiome(x, z); if (biome == NULL) return NULL; return biome->getMobs(mobCategory);}Step 5: Create the PurpleDimension class
Section titled “Step 5: Create the PurpleDimension class”Now the dimension itself. This ties together the chunk source, the fog color, sky behavior, and spawn rules.
PurpleDimension.h
#pragma once#include "Dimension.h"
class PurpleDimension : public Dimension{public: virtual void init(); virtual ChunkSource *createRandomLevelSource() const; virtual float getTimeOfDay(__int64 time, float a) const; virtual float *getSunriseColor(float td, float a); virtual Vec3 *getFogColor(float td, float a) const; virtual bool isNaturalDimension(); virtual bool mayRespawn() const; virtual bool hasGround(); virtual float getCloudHeight(); virtual bool isValidSpawn(int x, int z) const; virtual Pos *getSpawnPos(); virtual int getSpawnYPosition(); virtual bool isFoggyAt(int x, int z); virtual bool hasBedrockFog(); virtual double getClearColorScale();};PurpleDimension.cpp
#include "stdafx.h"#include "PurpleDimension.h"#include "PurpleChunkSource.h"#include "FixedBiomeSource.h"#include "net.minecraft.world.level.biome.h"
void PurpleDimension::init(){ biomeSource = new FixedBiomeSource(Biome::purple, 0.5f, 0.0f); id = 3; hasCeiling = false; ultraWarm = false;}
ChunkSource *PurpleDimension::createRandomLevelSource() const{ return new PurpleChunkSource(level, level->getSeed());}Time of day
Section titled “Time of day”Return a constant 0.75f for permanent sunrise/twilight. This gives the dimension a perpetual dim purple mood without being pitch black.
float PurpleDimension::getTimeOfDay(__int64 time, float a) const{ return 0.75f; // permanent twilight}For reference, the Aether uses 0.0f (permanent noon) and the Nether uses 0.5f (permanent midnight). Check Custom Dimensions for the full table.
Fog color
Section titled “Fog color”This is where the purple identity comes from. Return a static purple fog color. Since time of day is locked, there is no need to modulate by brightness.
Vec3 *PurpleDimension::getFogColor(float td, float a) const{ float r = 0.35f; float g = 0.10f; float b = 0.50f; return Vec3::newTemp(r, g, b);}These values give a rich dark purple. If you want something lighter, bump all three channels up. If you want it to pulse or shift over time, multiply by a Mth::sin() of td. See Fog & Sky for examples of time-varying fog.
Sunrise and sky settings
Section titled “Sunrise and sky settings”No sunrise effect. The dimension has permanent twilight, so a sunrise gradient would look wrong.
float *PurpleDimension::getSunriseColor(float td, float a){ return NULL;}Spawn and world behavior
Section titled “Spawn and world behavior”bool PurpleDimension::isNaturalDimension(){ return false; // no normal day/night mob spawning}
bool PurpleDimension::mayRespawn() const{ return false; // dying sends you back to Overworld}
bool PurpleDimension::hasGround(){ return true; // has a ground plane for rendering}
float PurpleDimension::getCloudHeight(){ return (float)Level::genDepth + 16; // clouds slightly above normal}
bool PurpleDimension::isValidSpawn(int x, int z) const{ int topTile = level->getTopTile(x, z); if (topTile == 0) return false; return Tile::tiles[topTile]->material->blocksMotion();}
Pos *PurpleDimension::getSpawnPos(){ return new Pos(0, 64, 0);}
int PurpleDimension::getSpawnYPosition(){ return 64;}
bool PurpleDimension::isFoggyAt(int x, int z){ return true; // thick fog everywhere, like the Nether}
bool PurpleDimension::hasBedrockFog(){ return false; // no bedrock-level fog darkening}
double PurpleDimension::getClearColorScale(){ return 1.0; // full brightness sky, no underground dimming}Setting isFoggyAt to true pulls the fog close, which makes the purple fog thick and visible. If you want a more open feel, return false and the fog will stay at normal render distance.
Step 6: Create the Purple Portal tile
Section titled “Step 6: Create the Purple Portal tile”The portal is a HalfTransparentTile that sits inside a frame and teleports entities. This tutorial uses an obsidian frame activated by right-clicking with a purple stone block, but you can use any frame material and activation method you like.
PurplePortalTile.h
#pragma once#include "HalfTransparentTile.h"
class Level;class Entity;
class PurplePortalTile : public HalfTransparentTile{public: PurplePortalTile(int id);
bool trySpawnPortal(Level *level, int x, int y, int z, bool actuallySpawn); virtual void entityInside(Level *level, int x, int y, int z, shared_ptr<Entity> entity); virtual int getResource(int data, Random *random, int playerBonusLevel);};PurplePortalTile.cpp
#include "stdafx.h"#include "PurplePortalTile.h"#include "net.minecraft.world.level.h"#include "net.minecraft.world.entity.h"
PurplePortalTile::PurplePortalTile(int id) : HalfTransparentTile(id, L"purplePortal", Material::portal, false){ setTicking(true);}
int PurplePortalTile::getResource(int data, Random *random, int playerBonusLevel){ return 0; // portal blocks don't drop anything}Frame validation and spawning
Section titled “Frame validation and spawning”The portal frame is 4 wide by 5 tall, made of obsidian. The interior is 2 wide by 3 tall. This follows the same pattern as the Aether portal (which uses glowstone). Adjust the frame block check if you want a different material.
bool PurplePortalTile::trySpawnPortal(Level *level, int x, int y, int z, bool actuallySpawn){ // Figure out portal orientation int xd = 0, zd = 0; if (level->getTile(x - 1, y, z) == Tile::obsidian_Id || level->getTile(x + 1, y, z) == Tile::obsidian_Id) xd = 1; if (level->getTile(x, y, z - 1) == Tile::obsidian_Id || level->getTile(x, y, z + 1) == Tile::obsidian_Id) zd = 1; if (xd == zd) return false; // must be oriented one way, not both
// Validate the full frame for (int xx = -1; xx <= 2; xx++) { for (int yy = -1; yy <= 3; yy++) { bool isCorner = (xx == -1 || xx == 2) && (yy == -1 || yy == 3); if (isCorner) continue; // skip corners
bool isEdge = (xx == -1) || (xx == 2) || (yy == -1) || (yy == 3); int t = level->getTile(x + xd * xx, y + yy, z + zd * xx);
if (isEdge) { if (t != Tile::obsidian_Id) return false; } else { if (t != 0) return false; // interior must be air } } }
if (!actuallySpawn) return true;
// Fill the interior with portal blocks level->noNeighborUpdate = true; for (int xx = 0; xx < 2; xx++) { for (int yy = 0; yy < 3; yy++) { level->setTile(x + xd * xx, y + yy, z + zd * xx, Tile::purplePortalTile_Id); } } level->noNeighborUpdate = false; return true;}Entity teleportation
Section titled “Entity teleportation”When an entity walks into the portal block, set a flag that gets picked up in the player tick.
void PurplePortalTile::entityInside(Level *level, int x, int y, int z, shared_ptr<Entity> entity){ if (entity->riding == NULL && entity->rider.lock() == NULL) { entity->handleInsidePurplePortal(); }}Step 7: Wire up portal teleportation on the player
Section titled “Step 7: Wire up portal teleportation on the player”You need to add a few things to the entity and player classes.
Entity.h
Section titled “Entity.h”Add a virtual method with an empty body:
virtual void handleInsidePurplePortal() {}Player.h
Section titled “Player.h”Add a flag:
bool isInsidePurplePortal = false;Override the method:
virtual void handleInsidePurplePortal();Player.cpp
Section titled “Player.cpp”void Player::handleInsidePurplePortal(){ if (changingDimensionDelay > 0) { changingDimensionDelay = 10; return; } isInsidePurplePortal = true;}ServerPlayer tick
Section titled “ServerPlayer tick”In ServerPlayer’s tick method, add a block alongside the existing Nether/End/Aether portal handling:
else if (isInsidePurplePortal){ portalTime += 1 / 80.0f; if (portalTime >= 1) { portalTime = 1; changingDimensionDelay = 10;
// Toggle between Overworld (0) and Purple Dimension (3) int targetDimension = (dimension == 3) ? 0 : 3;
server->getPlayers()->toggleDimension( dynamic_pointer_cast<ServerPlayer>(shared_from_this()), targetDimension); } isInsidePurplePortal = false;}The portalTime counter takes about 4 seconds (80 ticks) to fill. The player sees the portal overlay during this time, then gets teleported.
Step 8: Portal activation trigger
Section titled “Step 8: Portal activation trigger”You need a way to light the portal. The simplest approach: when the player right-clicks an obsidian frame with a purple stone block, try to spawn the portal.
In PurpleStoneTile, you could add a useOn override, but the cleaner place is in the item’s useOn method. If purple stone has a standard TileItem, add this check to TileItem::useOn() or create a custom item subclass.
A simpler option is to add the check in BucketItem.cpp like the Aether does, or in the use handler of whatever item you want as the activator. Here is the pattern:
// When the player uses purple stone on a block:if (itemId == Tile::purpleStone_Id){ // Check if we're clicking inside an obsidian frame if (level->getTile(xt, yt - 1, zt) == Tile::obsidian_Id || level->getTile(xt, yt, zt - 1) == Tile::obsidian_Id || level->getTile(xt, yt, zt + 1) == Tile::obsidian_Id) { if (Tile::purplePortalTile->trySpawnPortal(level, xt, yt, zt, true)) { return true; } }}Where you put this depends on your activation method. The Aether puts its check in BucketItem (water bucket inside glowstone frame). You could put yours in a custom item’s useOn, or hook into the tile placement logic.
Step 9: Register everything
Section titled “Step 9: Register everything”Here is a checklist of every file you need to touch.
Tile.h and Tile.cpp
Section titled “Tile.h and Tile.cpp”Already covered in Steps 1 and 2. You also need the portal tile:
In Tile.h:
class PurplePortalTile;static PurplePortalTile *purplePortalTile;static const int purplePortalTile_Id = 202;In Tile.cpp:
PurplePortalTile *Tile::purplePortalTile = NULL;
// Inside Tile::staticCtor():Tile::purplePortalTile = (PurplePortalTile *) ((new PurplePortalTile(202)) ->setDestroyTime(-1) ->setSoundType(Tile::SOUND_GLASS) ->setLightEmission(0.75f)) ->setTextureName(L"purplePortal");The -1 destroy time makes the portal unbreakable by hand, like vanilla portals.
Biome.h and Biome.cpp
Section titled “Biome.h and Biome.cpp”Already covered in Step 3.
Dimension.cpp (factory method)
Section titled “Dimension.cpp (factory method)”Add your dimension to Dimension::getNew():
Dimension *Dimension::getNew(int id){ if (id == -1) return new HellDimension(); if (id == 0) return new NormalDimension(); if (id == 1) return new TheEndDimension(); if (id == 2) return new AetherDimension(); if (id == 3) return new PurpleDimension(); // <-- add this return NULL;}Entity.h
Section titled “Entity.h”Add the empty virtual:
virtual void handleInsidePurplePortal() {}Player.h and Player.cpp
Section titled “Player.h and Player.cpp”Add the flag and override as shown in Step 7.
ServerPlayer.cpp
Section titled “ServerPlayer.cpp”Add the portal tick handling as shown in Step 7.
cmake/Sources.cmake
Section titled “cmake/Sources.cmake”Add all your new source files:
PurpleStoneTile.cppPurpleGrassTile.cppPurpleBiome.cppPurpleChunkSource.cppPurpleDimension.cppPurplePortalTile.cppStep 10: Add textures
Section titled “Step 10: Add textures”You need texture files for:
purpleStone- a stone-like texture in purple tonespurpleGrass- a grass texture with purple coloring (top, sides, bottom)purplePortal- the portal swirl effect (you can reuse the nether portal texture and recolor it)
See Block Textures for how to add entries to the texture atlas and set up multi-face textures for the grass block.
Full file list
Section titled “Full file list”Here is every file involved, for quick reference:
| File | Action |
|---|---|
PurpleStoneTile.h/.cpp | New: purple stone block class |
PurpleGrassTile.h/.cpp | New: purple grass block class |
PurpleBiome.h/.cpp | New: biome with purple colors and surface |
PurpleChunkSource.h/.cpp | New: terrain generator |
PurpleDimension.h/.cpp | New: dimension class |
PurplePortalTile.h/.cpp | New: portal block |
Tile.h | Modified: add static pointers and IDs for 3 new tiles |
Tile.cpp | Modified: add static definitions and registrations |
Biome.h | Modified: add static Biome *purple |
Biome.cpp | Modified: add biome registration |
Dimension.cpp | Modified: add id == 3 case to factory |
Entity.h | Modified: add handleInsidePurplePortal() virtual |
Player.h | Modified: add portal flag and override |
Player.cpp | Modified: implement handleInsidePurplePortal() |
ServerPlayer.cpp | Modified: add portal tick logic |
cmake/Sources.cmake | Modified: add new source files |
Testing it
Section titled “Testing it”- Build the project
- Start a world in creative mode
- Place an obsidian frame (4 wide, 5 tall, same shape as a nether portal)
- Activate it with your chosen method (right-click with purple stone, or whatever you wired up)
- Step into the portal and wait about 4 seconds
- You should arrive in the Purple Dimension with floating purple islands and thick purple fog
If something goes wrong:
- Check the dimension ID in
Dimension::getNew()matches what you set inPurpleDimension::init() - Make sure the biome ID does not conflict with existing biomes
- Make sure the tile IDs do not conflict with existing tiles
- Check that
handleInsidePurplePortal()is being called (add a log statement)
What to try next
Section titled “What to try next”Once the basic dimension works, here are some things to build on top of it:
- Custom ores: Add purple ore tiles that generate inside purple stone. Use
OreFeaturein your biome decorator withTile::purpleStone_Idas the replacement target. See Adding Blocks for the ore tile pattern. - Custom trees: Create a tree feature that generates purple wood and purple leaves. Override
getTreeFeature()on your biome. - Ambient particles: Add floating purple particles using
animateTick()on your purple grass tile. See Custom Particles. - Custom structures: Place structures like towers or ruins during
postProcess(). See Custom Structures. - Custom mobs: Add enemies and friendlies to the biome’s mob lists. See Adding Entities.
- Better terrain: Tweak the noise parameters in
getHeights()for different island sizes and shapes. Add a carving noise layer to cut holes into the terrain. - Custom light ramp: Override
updateLightRamp()onPurpleDimensionto give the dimension a purple ambient glow, similar to how the Nether has a faint red ambient light. - Portal particle effects: Add a purple particle effect around the portal frame. The Nether portal does this with
animateTick(). - ColourTable integration: Add a
Purple_Fog_Colourentry to the colour table so texture packs can override your fog color without code changes. See Fog & Sky for how the colour table works.