Jump to content
  • entries
    7
  • comments
    3
  • views
    1,253

C++ Ultra Beginner's Guide #4 - adding features to strategy game - custom save/load system, particles, decals, sounds


Dreikblack

27 views

 Share

In this tutorial we will add features to real-time tactic/strategy game in Ultra Engine fro prev tutorial:

Plan:

1. Add flag model to use when pointing where to go for units

2. Make save/load system with quick save

3. Make blood particle emitter which will be used on hits and use it in Unit::Methood

4. Make blood decals which will be on the ground after death and spawn it in Unit::Kill

5. Download and use sounds for units getting damage.

 

In this tutorial used 2117 Build from Steam Beta branch (Dev in Standalone version). In 0.9.8 version Component::Load/Save are protected.

Build number can be found in the Editor Help->About

Flag

Flag will represent a place that player units will try to reach.

Model can be found here: https://sketchfab.com/3d-models/triangle-flag-adadd59fd56d44f7b0354d1caba38f95

Download .glb version and put it to new folder "triangle_flag"  in Models.

Convert to mdl in the Editor. By default this model is too small. Set Scale 5.0 in Transform tab and in Tools click Reset Transform (works properly atm only for non-animated models) to save new size for good.

Also click on Collapse in Tools to unite all meshes so it would be shown like single entity in a scene (otherwise this entity will have some kids which is not critical, but it's better to make it simpler).

image.thumb.png.fc3fa9f41d8353916249c02ef53b5565.png

triangle_flag.zip

Flag prefab:

1. Put triangle_flag model to scene

2. In Physics Collision type - None and Pick Mode - None

3. Attach WayPoint component

4. In Appearance tab in add "Save" to tags - it will be needed eventually for game save/load system

5. Save as prefab called FlagWayPoint in Prefabs folder

image.png.348a5a6537c6aa84d1c6c6364f06b569.png

FlagWayPoint.zip

Updating classes

Let's update Unit class for using getting an entity as target point and save system later.

Add new method to Unit.h: 

void goTo(shared_ptr<Entity> targetPointEntity, bool isForced = false);

In Unit.cpp method implementation:

void Unit::goTo(shared_ptr<Entity> targetPointEntity, bool isForced) {
	if (targetPointEntity) {
		isForcedMovement = isForced;
		targetPoint = targetPointEntity;
		goTo();
	}
}

 

Add to Start() a new tag:

entity->AddTag("Save");

And also in Start() after that:

if (seq != -1) {
            //to disable pain state at end of pain animation
            model->skeleton->AddHook(seq, count - 1, EndPainHook, Self());
}

Add this:

		if (health <= 0) {
			seq = model->FindAnimation(deathName);
			int count = model->CountAnimationFrames(seq);
			model->Animate(deathName, 1.0f, 250, ANIMATION_ONCE, count - 1);
		}

It's needed to make Unit make lie down in death animation if it was killed when game was save.

 

Add a little bit below after healthBar initialization nex tline:

healthBar->SetScale((float)health / (float)maxHealth, 1, 1);

So health bar would have correct size after a game loading.

 

We also want to save selected state after a loading so add first line to Load and second to Save:

if (properties["isSelected"].is_boolean()) isSelected = properties["isSelected"];
properties["isSelected"] = isSelected;

 

After line this line in Kill method:

ListenEvent(EVENT_TIMERTICK, removeEntityTimer, RemoveEntityCallback, Self());

Add this:

//not saving if supposed to be deleted anyway
entity->RemoveTag("Save");

Current Unit: Unit.zip

 

First version of TopDownCamera had an issue that would appear after game load:

Update init() method to make it update pivot after game load:

	if (gameCamera && !targetPivot.lock()) {
		gameCamera->Listen();//for positional sound 
		gameCamera->SetSweptCollision(true);//for removing pop up effect after quick move/turn
		gameCamera->SetRotation(CAMERA_PITCH, gameCamera->GetRotation(true).y, gameCamera->GetRotation(true).z);
		auto targetPivotShared = CreatePivot(gameCamera->GetWorld());
		targetPivotShared->SetPickMode(PICK_NONE);	
		sceneWeak.lock()->AddEntity(targetPivotShared);
		targetPivot = targetPivotShared;
	}
	//setting position and rotation here in case of game load
	gameCamera->SetParent(nullptr);
	auto targetPivotShared = targetPivot.lock();
	auto targetPosition = getCirleCenter(gameCamera->GetPosition(true), gameCamera->GetRotation(true));
	targetPosition.y = 0;
	targetPivotShared->SetPosition(targetPosition);
	targetPivotShared->SetRotation(0, gameCamera->GetRotation(true).y, gameCamera->GetRotation(true).z);
	gameCamera->SetParent(targetPivotShared);
	return true;

TopDownCamera.zip

 

In StrategyController.cpp update Start to add tag for Save next to another tag line

entity->AddTag("Save");

 

To Save method add saving selected units:

	properties["selectedUnits"] = {};
	int index = 0;
	for (auto const& selectedUnitWeak : selectedUnits) {
		auto selectedUnit = selectedUnitWeak.lock();
		if (selectedUnit) {
			properties["selectedUnits"][index] = selectedUnit->GetUuid();
			index++;
		}
	}

 

Add their loading to Load:

	selectedUnits.clear();
	if (properties["selectedUnits"].is_array()) {
		for (int i = 0; i < properties["selectedUnits"].size(); i++) {
			auto unit = scene->GetEntity(properties["selectedUnits"][i]);
			if (unit) {
				selectedUnits.push_back(unit);
			}
		}
	}

 

Find this line

for (auto const& entityWeak : selectedUnits) {

Replace a code from starting from line above this line and ended 8 lines lower with this snippet:

} else if (!selectedUnits.empty()) {
				auto flag = LoadPrefab(camera->GetWorld(), "Prefabs/FlagWayPoint.pfb");
				if (flag) {
					flag->SetPosition(pick.position);
				}
				for (auto const& entityWeak : selectedUnits) {
					auto entityUnit = entityWeak.lock();
					if (entityUnit && entityUnit->GetComponent<Unit>()) {
						if (flag) {
							entityUnit->GetComponent<Unit>()->goTo(flag, true);
						} else {
							entityUnit->GetComponent<Unit>()->goTo(pick.position, true);
						}
					}
				}
}

Or just download full class: StrategyController.zip

Custom Save/Load system

Current official system have a lot of flaws which are listed here:

 

Most of them will be fixed, but something like no loading entities, which were added while run time like flags in this game, may still persist. This one is resolved by loading prefabs. You can also add manual recreation for most types of entities, depends on needs, but prefabs should be enough in most cases.

Game class have a lot of changes, which were made to create custom save system.

Whole Game.h:

#pragma once
#include "UltraEngine.h"
#include "Components/Player/FPSPlayer.h"

using namespace UltraEngine;

class Game : public Object {
protected:
	shared_ptr<Camera> uiCamera;
	shared_ptr<Scene> scene;
	shared_ptr<FPSPlayer> fpsPlayer;
	shared_ptr<Widget> menuPanel;
	shared_ptr<Widget> gameSavedLabel;
	//hide gameSavedLabel on timer
	shared_ptr<Timer> gameSavedLabelTimer;
	Game();
	void init(shared_ptr<Framebuffer> framebuffer, WString mapPath);
	//update basic entity properties - position, rotation, tags
	void loadEntity(shared_ptr<Entity> entity, table entityTable);
	void loadGame(table saveTable);
	void saveGame(WString saveName);
	static bool QuickSaveGameCallback(const UltraEngine::Event& ev, shared_ptr<UltraEngine::Object> extra);
	static bool HideGameSavedLabelCallback(const UltraEngine::Event& ev, shared_ptr<UltraEngine::Object> extra);
public:
	//to show/hide game menu on Esc
	static bool GameMenuButtonCallback(const Event& ev, shared_ptr<Object> extra);
	static std::shared_ptr<Game> create(shared_ptr<Framebuffer> framebuffer, WString mapPath);
	//for game loading
	static std::shared_ptr<Game> create(shared_ptr<Framebuffer> framebuffer, table saveTable);
	shared_ptr<Interface> ui;
	shared_ptr<World> world;
};

Game.cpp:

#include "UltraEngine.h"
#include "Game.h"
#include "CustomEvents.h"

Game::Game() = default;

std::shared_ptr<Game> Game::create(shared_ptr<Framebuffer> framebuffer, WString mapPath) {
	struct Struct : public Game {};
	auto instance = std::make_shared<Struct>();
	instance->init(framebuffer, mapPath);
	return instance;
}

std::shared_ptr<Game> Game::create(shared_ptr<Framebuffer> framebuffer, table saveTable) {
	struct Struct : public Game {};
	auto instance = std::make_shared<Struct>();
	std::string mapName = saveTable["MapPath"];
	instance->init(framebuffer, WString(mapName));
	instance->loadGame(saveTable);
	return instance;
}

bool Game::GameMenuButtonCallback(const Event& ev, shared_ptr<Object> extra) {
	if (KEY_ESCAPE == ev.data && extra) {
		auto game = extra->As<Game>();
		bool isHidden = game->menuPanel->GetHidden();
		game->menuPanel->SetHidden(!isHidden);
		if (game->fpsPlayer) {
			//we can get a game window anywhere, but take in mind that it will return nullptr, if window is not active ;)
			auto window = ActiveWindow();
			//checking just in case if we actually got a window
			if (window) {
				//hiding cursor when hiding a menu and vice versa
				window->SetCursor(isHidden ? CURSOR_DEFAULT : CURSOR_NONE);
			}
			game->fpsPlayer->doResetMousePosition = !isHidden;
		}
		//If the callback function returns false no more callbacks will be executed and no event will be added to the event queue.
		//to avoid double call
		return false;
	}
	return true;
}

static bool MainMenuButtonCallback(const Event& ev, shared_ptr<Object> extra) {
	EmitEvent(EVENT_MAIN_MENU);
	return true;
}

static bool ExitButtonCallback(const Event& ev, shared_ptr<Object> extra) {
	exit(0);
	return true;
}

void Game::init(shared_ptr<Framebuffer> framebuffer, WString mapPath) {
	world = CreateWorld();
	scene = LoadMap(world, mapPath);
	for (auto const& entity : scene->entities) {
		auto foundPlayer = entity->GetComponent<FPSPlayer>();
		if (foundPlayer) {
			fpsPlayer = foundPlayer;
			break;
		}
	}
	auto font = LoadFont("Fonts/arial.ttf");
	//Create user interface for game menu
	auto frameSize = framebuffer->GetSize();
	ui = CreateInterface(world, font, frameSize);
	ui->SetRenderLayers(2);
	ui->root->SetColor(0.0f, 0.0f, 0.0f, 0.0f);
	uiCamera = CreateCamera(world, PROJECTION_ORTHOGRAPHIC);
	uiCamera->SetPosition(float(frameSize.x) * 0.5f, float(frameSize.y) * 0.5f, 0);
	uiCamera->SetRenderLayers(2);
	uiCamera->SetClearMode(CLEAR_DEPTH);
	//widgets are stays without extra shared pointers because parent widet, ui->root in this case, keep them
	//to remove widget you should do widget->SetParent(nullptr)
	menuPanel = CreatePanel(frameSize.width / 2 - 150, frameSize.height / 2 - 300 / 2, 300, 250, ui->root);
	gameSavedLabel = CreateLabel("GAME SAVED", frameSize.width / 2 - 100, 50, 200, 30, ui->root);
	gameSavedLabel->SetFontScale(2.0f);
	gameSavedLabel->SetHidden(true);
	auto menuButton = CreateButton("Main menu", 50, 50, 200, 50, menuPanel);
	ListenEvent(EVENT_WIDGETACTION, menuButton, MainMenuButtonCallback);
	auto exitButton = CreateButton("Exit", 50, 150, 200, 50, menuPanel);
	ListenEvent(EVENT_WIDGETACTION, exitButton, ExitButtonCallback, nullptr);
	//we don't need game menu on screen while playing
	menuPanel->SetHidden(true);
	//and we will need it once hitting Esc button
	ListenEvent(EVENT_KEYUP, nullptr, GameMenuButtonCallback, Self());
	//take in mind that extra param will be kept as shared_ptr in callback ^
	ListenEvent(EVENT_KEYUP, nullptr, QuickSaveGameCallback, Self());
}

void Game::loadEntity(shared_ptr<Entity> entity, table entityTable) {
	if (entityTable["position"].is_array() && entityTable["position"].size() == 3) {
		entity->SetPosition(entityTable["position"][0], entityTable["position"][1], entityTable["position"][2], true);
	}
	if (entityTable["rotation"].is_array() && entityTable["rotation"].size() == 3) {
		entity->SetRotation(entityTable["rotation"][0], entityTable["rotation"][1], entityTable["rotation"][2], true);
	}
	if (entityTable["tags"].is_array()) {
		for (int i = 0; i < entityTable["tags"].size(); i++) {
			entity->AddTag(std::string(entityTable["tags"][i]));
		}
	}
}

void Game::loadGame(table saveTable) {
	//old-new entity id
	vector<std::pair<String, String>> uuids;
	std::set<String> newEntities;
	//entites that was not in scene will be deleted once Components will be loaded and gets needed entities
	std::set<shared_ptr<Entity>> entitiesToRemoveFromScene;
	//iterating std::map by key (uuid) and value (entityTable) instead of pair
	for (auto& [uuid, entityTable] : saveTable["SavedEntities"]) {
		auto entity = scene->GetEntity(uuid);
		//load properties for saved entity that was initially on map
		if (entity) {
			loadEntity(entity, entityTable);
		//or if it was not we can recreate prefab at least
		} else if (entityTable["prefabPath"].is_string()) {
			//spawn saved entity that was not initially on map
			auto spawnedEntity = LoadPrefab(world, String(entityTable["prefabPath"]));
			if (!spawnedEntity) {
				continue;
			}
			scene->AddEntity(spawnedEntity);
			if (entityTable["isInScene"].is_boolean() && !entityTable["isInScene"]) {
				entitiesToRemoveFromScene.insert(spawnedEntity);
			}
			loadEntity(spawnedEntity, entityTable);
			uuids.push_back(std::pair(uuid, spawnedEntity->GetUuid()));
			newEntities.insert(spawnedEntity->GetUuid());
		}
	}
	//delete not saved entities
	for (auto const& entity : world->GetTaggedEntities("Save")) {
		//does newEntities containes curent entity
		if (newEntities.find(entity->GetUuid()) != newEntities.end()) {
			//skip new entity
			continue;
		}
		//if supposed to be saved and was not due being removed when save was made then remove it from scene
		table entityTable = saveTable["SavedEntities"][entity->GetUuid()];
		if (entityTable.empty()) {
			scene->RemoveEntity(entity);
		}
	}
	//saving table++ as a string
	auto saveString = String(saveTable.to_json());
	//replace entities ids so component would use new ones
	for (auto const& [oldUuid, newUuid] : uuids) {
		saveString = saveString.Replace(oldUuid, newUuid);
	}
	//converting back to table++ from string
	saveTable = ParseJson(saveString);
	//Load saved data to components
	for (auto const& entity : world->GetTaggedEntities("Save")) {
		auto& entityTable = saveTable["SavedEntities"][entity->GetUuid()];
		for (auto const& component : entity->components) {
			component->Load(entityTable, nullptr, scene, LOAD_DEFAULT, nullptr);
		}
	}
	//starting components now when all data is there
	for (auto const& entity : world->GetTaggedEntities("Save")) {
		for (auto const& component : entity->components) {
			component->Start();
		}	
	}
	//removing from scene entites that scene had not
	for (auto const& entity : entitiesToRemoveFromScene) {
		scene->RemoveEntity(entity);
	}
}

void Game::saveGame(WString saveName) {
	table saveTable;
	//saving map path to use it later to load correct map
	saveTable["MapPath"] = RelativePath(scene->path).ToUtf8String();
	saveTable["SavedEntities"] = {};
	for (auto const& entity : world->GetTaggedEntities("Save")) {
		table entityTable;
		for (auto const& component : entity->components) {
			component->Save(entityTable, nullptr, scene, SAVE_DEFAULT, nullptr);
		}
		//just to make save file more readable
		if (!entity->name.empty()) {
			entityTable["name"] = entity->name.ToUtf8String();
		}
		//saving position and rotation of entity to restore them in Load
		auto position = entity->GetPosition(true);
		entityTable["position"] = {};
		entityTable["position"][0] = position.x;
		entityTable["position"][1] = position.y;
		entityTable["position"][2] = position.z;
		auto rotation = entity->GetRotation(true);
		entityTable["rotation"] = {};
		entityTable["rotation"][0] = rotation.x;
		entityTable["rotation"][1] = rotation.y;
		entityTable["rotation"][2] = rotation.z;
		entityTable["tags"] = {};
		//to remove it from scene later once everything restored inc case if only components supposed to keep this entity
		entityTable["isInScene"] = scene->GetEntity(entity->GetUuid()) ? true : false;
		int tagIndex = 0;
		for (auto& tag : entity->tags) {
			entityTable["tags"][tagIndex] = tag.ToUtf8String();
			tagIndex++;
		}
		//save prefab path to be able restore entity if it was added to scene later as prefab
		auto prefab = entity->GetPrefab();
		if (prefab) {
			entityTable["prefabPath"] = RelativePath(prefab->GetPath()).ToUtf8String();
		}
		//using entity id as key for its properties
		saveTable["SavedEntities"][entity->GetUuid()] = entityTable;
	}
	SaveTable(saveTable, saveName);
	//showing "Game Saved" labl for 2 seconds
	gameSavedLabel->SetHidden(false);
	gameSavedLabelTimer = UltraEngine::CreateTimer(2000);
	ListenEvent(EVENT_TIMERTICK, gameSavedLabelTimer, HideGameSavedLabelCallback, Self());
}

bool Game::QuickSaveGameCallback(const UltraEngine::Event& ev, shared_ptr<UltraEngine::Object> extra) {
	if (KEY_F5 == ev.data && extra) {
		auto game = extra->As<Game>();
		game->saveGame("QuickSave.save");
	}
	return true;
}

bool Game::HideGameSavedLabelCallback(const UltraEngine::Event& ev, shared_ptr<UltraEngine::Object> extra) {
	if (extra && extra->As<Game>()) {
		auto game = extra->As<Game>();
		game->gameSavedLabel->SetHidden(true);
		game->gameSavedLabelTimer->Stop();
		game->gameSavedLabelTimer = nullptr;
	}
	return false;
}

Game.zip

Add new functions to main.cpp:

void LoadGame(WString savePath) {
	table saveTable = LoadTable(savePath);
	if (saveTable == nullptr) {
		return;
	}
	menu.reset();
	//to avoid random error in World::Update()
	game.reset();
	loadingWorld->Render(framebuffer);
	if (currentWorld) currentWorld.reset();
	if (currentUi) currentUi.reset();
	game = Game::create(framebuffer, saveTable);
	currentWorld = game->world;
	currentUi = game->ui;
}

bool QuickLoadGameCallback(const Event& event, shared_ptr<Object> extra) {
	if (KEY_F9 == event.data) {
		LoadGame("QuickSave.save");
	}
	return true;
}

 

And add ListenEvent next to other similar lines:

ListenEvent(EVENT_KEYUP, nullptr, QuickLoadGameCallback);

 main.zip

 

Now you can save and load game with F5 and F9 respectively

Making Particle Effect

Download this blood texture made by TobiasMhttps://opengameart.org/content/blood-splat

It's not optimal for particle emitter, but good enough for learning purpose.

Put into Materials\Particles folder and convert to .dds in the Editor.

Generate a material from dds texture.

Add to any scene particle emitter:

image.png.17517882aee1c20f512ead245057b9c2.png

In Appearance tab of Particles choose a recently made material.

Now we able to see result when we will change this material properties.

Open material:

1. Choose Lambertian shader family - it has better perfomance than PBR and it can make a difference eventually with many particle emitters which produce dozens particles.

2. In Blend tab check Transparent checkbox.

Now it particles would look like that:

image.png.b3d986ac42d0acd8ca80398dd8b499a9.png

Open Particles tab of Particle Emitter in entity properties:

1. Particle count - how many particles of this emitter exist at same time, left it as 50

2. Emission shape - shape of area where particles will be spawn

3. Emission Area - size of this area (width, height, depth). If 0 then particles spawns at same point. Left it as 0.

4. Burst frequency - how often particles spawns. Will be 800 

5. Burst count - how much particles spawns in every burst. Enter 50

6. Colors - how particle looks at spawn and despawn

7. Velocity - direction and speed - make it 0

8. Acceleration - same but it's accel

9. Size - width and height of particle. No depth, because every particle is just a billboard sprite. Makie it 0.2/0.2

10. Radius. First value - relative size (i.e. 1.0 is 100%) for particle at start and second value is size when particles despawns. Value - 1.0/1.0

11. Turbulence - random speed in random direction. With 0 velocity it will make particles move in different directions. Make it 300.0

12. Rotation speed - how fast particle rotate. Make it 0

Download ParticleEffect to ParticleEffect component to Components\Appearance folder. Include into project and add to ComponentSystem as usual.

Add this component to an emitter. It's needed to make blood hit effect to dissapear before it would burst again.

Temporary checkbox on and Duration 700.

Name emitter as BloodHit and save it as prefab "BloodHit.phb" in Prefabs folder

Let's use it for units. In Unit.cpp in Damage method add next code somewhere above auto now = world->GetTime();

auto bloodHit = LoadPrefab(world, "Prefabs/BloodHit.pfb");
auto entity = GetEntity();
if (bloodHit) {
	auto bloodHitPosition = entity->GetPosition(true);
	//to lift it up to entity center
	bloodHitPosition.y = bloodHitPosition.y + entity->GetBounds(BOUNDS_GLOBAL).size.height / 2;
	bloodHit->SetPosition(bloodHitPosition, true);
	//prefabs component are not started after its load so will do it manually
	for (auto const& component : bloodHit->components) {
		component->Start();
	}
}

Making decal

Copy blood_splat.dds texture to Materials\Decals folder. Generate a new material from it and name BloodPuddle.

Edit this material:

1. Keep PBR as shader family.

2. In Blend tab check Transparent checkbox.

3. In Surface Metalness 0, and Roughness 50.

Add new decal to scene - 256x256 size with 16 height (or 250x250 and 20)

Choose new material in appearance.

Save this decal as prefab wiht BloodPuddle name.

 

We will spawn this decal after unit death. In Unit::Kill() method add above auto model = entity->As<Model>(); line following code:

auto scene = sceneWeak.lock();
auto bloodPuddle = LoadPrefab(entity->GetWorld(), "Prefabs/BloodPuddle.pfb");
if (bloodPuddle && scene) {
	auto bloodHitPosition = entity->GetPosition(true);
	bloodPuddle->SetPosition(bloodHitPosition, true);
	//to keep it
	scene->AddEntity(bloodPuddle);
}

Pain sounds

Download sounds for Warrok: https://opengameart.org/node/132772

And for Paladin: https://opengameart.org/content/pain-sounds-by-emopreben

In Sounds folder create Units subfolder and put there bear_02.ogg from 1st pack and "Painsounds v2 - Track 5 - Urggh.ogg" from 2nd

Add to properties in Unit.json:

      {
        "label": "Pain sound",
        "name": "painSound",
        "value": "",
        "filecategory": "SOUND"
      },

New member in Unit.h header:

	shared_ptr<Sound> painSound;

In Load method in Unit.cpp will load attached in the Editor sound:

	if (properties["painSound"].is_string()) painSound = LoadSound(std::string(properties["painSound"]));

And play this sound in Damage() above auto now = world->GetTime(); line:

if (painSound) {
	entity->EmitSound(painSound);
}

Unit class: FinalUnit.zip

 

Now open Paladin prefab and add their pain sound in Unit component. Same for Warror.

Delete units from Strategy map and add them again from prefabs. Remember to break prefab after that. 

strategy.zip

Cache

In debug mode you could notice micro-freezes when prefabs are loaded every time when it's not at the map.

Thats because unload from memory an assets that have no shared_pointer to them.

We can fix by making cache in Game class.

Add to Game.h new member:

vector<shared_ptr<Entity>> prefabCache;

And at the end of init method in Game.cpp:

	//caching
	prefabCache.push_back(LoadPrefab(world, "Prefabs/BloodPuddle.pfb"));
	prefabCache.push_back(LoadPrefab(world, "Prefabs/BloodHit.pfb"));
	prefabCache.push_back(LoadPrefab(world, "Prefabs/FlagWayPoint.pfb"));
	for (auto const& prefab : prefabCache) {
		if (prefab) {
			prefab->SetHidden(true);
		}
	}

GameCache.zip

Now Debug mode will be smoother since assets loaded on game load and not in run time when components uses them.

In Release mode it would be also necessary to do for heavy assets.

Final version on github: https://github.com/Dreikblack/CppTutorialProject/tree/4-save-load-particles-decals

 Share

0 Comments


Recommended Comments

There are no comments to display.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...