Hello,
i wanted to play with Urho3d to create a simple game, i’ve created a tictactoe with the base assets :
use UrhoPlayer with this .as script :
// TicTacToe 3D scene example.
// This sample demonstrates:
// - Creating a 3D scene with static content
// - Raytracing to select a node and use "vars" to store data
// - a simple who win algorithm
#include "Scripts/Utilities/Sample.as"
//You can play with this values
const float FLOOR_ELEMENT_SIZE = 4.0f; // Change scale of the game
const int ROW_FLOW_NUMBER=13; // Play area is a square, so row = column
const int REQUIRED_TO_WIN=6; // Number of aligned piece to win
const bool DISABLE_ROTATION=true; //If true, all the cell a aligned, and not moved on click
// ## Code is below
//Global var
Scene@ scene_;
Node@ cameraNode;
Controls camControl;
float yaw = 10.0f;
float pitch = 30.0f;
Array<Node@> tblPlateau;
Node@ plateauParent;
Text@ activePlayerText ;
const int OWNED_BY_NO_ONE=0;
bool gameCreated=false; //if you leave "e" or "s" it doens't spam the commande
bool drawDebug = false;
int activePlayer=1; // 1st played is the first to play :p
int numCasePlayed =0;
void Start()
{
// Execute the common startup for samples
SampleStart();
// Create the scene content
CreateScene();
// Create the UI content
CreateUI();
// Setup the viewport for displaying the scene
SetupViewport();
// Hook up to the frame update events
SubscribeToEvents();
// Start the game
startOfGame();
}
void CreateUI()
{
// Create a Cursor UI element because we want to be able to hide and show it at will. When hidden, the mouse cursor will
// control the camera, and when visible, it will point the raycast target
XMLFile@ style = cache.GetResource("XMLFile", "UI/DefaultStyle.xml");
Cursor@ cursor = Cursor();
cursor.SetStyleAuto(style);
ui.cursor = cursor;
// Set starting position of the cursor at the rendering window center
cursor.SetPosition(graphics.width / 2, graphics.height / 2);
// Construct new Text object, set string to display and font to use
Text@ instructionText = ui.root.CreateChild("Text");
instructionText.text =
"Use up,down,left,right arrow keys to move\n"
"Use \"e\" End the game, \"s\" re-start a game\n"
"LMB to place ticTacToe Element, RMB to rotate view\n"
"You need : "+REQUIRED_TO_WIN+" aligned to win \n"
"Space to move Up\n";
instructionText.SetFont(cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15);
// The text has multiple rows. Center them in relation to each other
instructionText.textAlignment = HA_CENTER;
// Position the text relative to the screen center
instructionText.horizontalAlignment = HA_CENTER;
instructionText.verticalAlignment = VA_CENTER;
instructionText.SetPosition(0, ui.root.height / 4);
activePlayerText = ui.root.CreateChild("Text");
activePlayerText.text ="player "+activePlayer;
activePlayerText.textAlignment = HA_CENTER;
activePlayerText.horizontalAlignment = HA_CENTER;
activePlayerText.verticalAlignment = VA_BOTTOM;
activePlayerText.SetPosition(0, 1);
activePlayerText.SetFont(cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"), 30);
activePlayerText.color = Color(0.0f, 1.0f, 0.0f);
}
void CreateScene()
{
scene_ = Scene();
// Create the Octree component to the scene. This is required before adding any drawable components, or else nothing will
// show up. The default octree volume will be from (-1000, -1000, -1000) to (1000, 1000, 1000) in world coordinates; it
// is also legal to place objects outside the volume but their visibility can then not be checked in a hierarchically
// optimizing manner
scene_.CreateComponent("Octree");
scene_.CreateComponent("DebugRenderer");
// Create a directional light to the world so that we can see something. The light scene node's orientation controls the
// light direction; we will use the SetDirection() function which calculates the orientation from a forward direction vector.
// The light will use default settings (white light, no shadows)
Node@ lightNode = scene_.CreateChild("DirectionalLight");
lightNode.direction = Vector3(0.6f, -1.0f, 0.8f); // The direction vector does not need to be normalized
Light@ light = lightNode.CreateComponent("Light");
light.lightType = LIGHT_DIRECTIONAL;
// Create a scene node for the camera, which we will move around
// The camera will use default settings (1000 far clip distance, 45 degrees FOV, set aspect ratio automatically)
cameraNode = scene_.CreateChild("Camera");
cameraNode.CreateComponent("Camera");
// Set an initial position for the camera scene node above the plane
cameraNode.position = Vector3((FLOOR_ELEMENT_SIZE*ROW_FLOW_NUMBER)/2, 10.0f, -15.0f);
cameraNode.Pitch(pitch);
cameraNode.Yaw(yaw);
}
void SetupViewport()
{
// Set up a viewport to the Renderer subsystem so that the 3D scene can be seen. We need to define the scene and the camera
// at minimum. Additionally we could configure the viewport screen size and the rendering path (eg. forward / deferred) to
// use, but now we just use full screen and default render path configured in the engine command line options
Viewport@ viewport = Viewport(scene_, cameraNode.GetComponent("Camera"));
renderer.viewports[0] = viewport;
}
void MoveCamera(float timeStep)
{
// Right mouse button controls mouse cursor visibility: hide when pressed
ui.cursor.visible = !input.mouseButtonDown[MOUSEB_RIGHT];
// Do not move if the UI has a focused element (the console)
if (ui.focusElement !is null)
return;
// Movement speed as world units per second
const float MOVE_SPEED = 20.0f;
// Mouse sensitivity as degrees per pixel
const float MOUSE_SENSITIVITY = 0.1f;
// Use this frame's mouse motion to adjust camera node yaw and pitch. Clamp the pitch between -90 and 90 degrees
// Only move the camera when the cursor is hidden
if (!ui.cursor.visible) {
IntVector2 mouseMove = input.mouseMove;
yaw += MOUSE_SENSITIVITY * mouseMove.x;
pitch += MOUSE_SENSITIVITY * mouseMove.y;
pitch = Clamp(pitch, -90.0f, 90.0f);
// Construct new orientation for the camera scene node from yaw and pitch. Roll is fixed to zero
cameraNode.rotation = Quaternion(pitch, yaw, 0.0f);
}
// Read WASD keys and move the camera scene node to the corresponding direction if they are pressed
//Currently setted for french keyboard
if (input.keyDown[KEY_UP])
cameraNode.TranslateRelative(Vector3(0.0f, 0.0f, 1.0f) * MOVE_SPEED * timeStep);
if (input.keyDown[KEY_DOWN])
cameraNode.TranslateRelative(Vector3(0.0f, 0.0f, -1.0f) * MOVE_SPEED * timeStep);
if (input.keyDown[KEY_LEFT])
cameraNode.TranslateRelative(Vector3(-1.0f, 0.0f, 0.0f) * MOVE_SPEED * timeStep);
if (input.keyDown[KEY_RIGHT])
cameraNode.TranslateRelative(Vector3(1.0f, 0.0f, 0.0f) * MOVE_SPEED * timeStep);
if (input.keyDown[KEY_D])
cameraNode.TranslateRelative(Vector3(0.0f, 2.0f, 0.0f) * MOVE_SPEED * timeStep);
//Check what is clicked, and if game engine need to react
if (ui.cursor.visible && input.mouseButtonPress[MOUSEB_LEFT])
whatHaveYouCLicked();
if (input.keyDown[KEY_E])
endOfGame();
if (input.keyDown[KEY_S])
startOfGame ();
//Active / Desactive debug
if (input.keyPress[KEY_SPACE])
drawDebug = !drawDebug;
}
void startOfGame()
{
if (gameCreated)
return;
plateauParent = scene_.CreateChild("damier");
//Create the "floor" with a list of node who will be valid target for raycasting
for (uint j = 0; j < ROW_FLOW_NUMBER; ++j) {
for (uint i = 0; i < ROW_FLOW_NUMBER; ++i) {
Node@ floorNode = plateauParent.CreateChild("damier");
//Setting vars who will help up to track what is doable and what is not
floorNode.vars["x"]=i;
floorNode.vars["y"]=j;
floorNode.vars["playedBy"]=OWNED_BY_NO_ONE; //Case not played
floorNode.position = Vector3(i*FLOOR_ELEMENT_SIZE+(FLOOR_ELEMENT_SIZE/2), 0.0f, j*FLOOR_ELEMENT_SIZE+(FLOOR_ELEMENT_SIZE/2));
//We re-use already present materials, they use 99% of the FLOOR_ELEMENT_SIZE
floorNode.scale=Vector3(FLOOR_ELEMENT_SIZE-(FLOOR_ELEMENT_SIZE/100),-(FLOOR_ELEMENT_SIZE/100),FLOOR_ELEMENT_SIZE-(FLOOR_ELEMENT_SIZE/100));
//create the static model
StaticModel@ floorStaticModel = floorNode.CreateComponent("StaticModel");
floorStaticModel.model = cache.GetResource("Model", "Models/Box.mdl");
floorStaticModel.material = cache.GetResource("Material", "Materials/Stone.xml");
//Save in an Array of Node@
tblPlateau.Push(floorNode);
}
}
/**
* Sample for iterating the plateau lineary and accessing each part
*/
if (!DISABLE_ROTATION){
for (uint i = 0; i < tblPlateau.length; ++i) {
tblPlateau[i].Rotate(Quaternion(0.0f, 30.0f, 0.0f));
Print(tblPlateau[i].vars["x"]);
}
}
//Define somes vars
gameCreated=true;
activePlayer=1;
activePlayerText.text ="player "+activePlayer;
}
void endOfGame()
{
//Need to add a test not to erase 50 times the game
tblPlateau.Clear();
plateauParent.RemoveAllChildren();
gameCreated=false;
}
void whatHaveYouCLicked()
{
Vector3 hitPos;
Drawable@ hitDrawable;
if (Raycast(250.0f, hitPos, hitDrawable)) {
// search with floor coords what cell is touched by the ray vector3 impact point
int caseXpos = (hitPos.x/FLOOR_ELEMENT_SIZE);
int caseYpos = (hitPos.z/FLOOR_ELEMENT_SIZE);
//2nd method : Selected Node
Node@ selectedNode = hitDrawable.node;
//Online node named "damier" are valide target
if (selectedNode.name =="damier") {
if (selectedNode.vars["playedBy"].GetUInt()==0) {
//search area by vector3D coordinates
int caseX = selectedNode.vars["x"].GetUInt();
int caseY = selectedNode.vars["y"].GetUInt();
int indice=caseY*ROW_FLOW_NUMBER+caseX; //linearize the coordonnates
if (!DISABLE_ROTATION && indice<=tblPlateau.length) {
tblPlateau[indice].Rotate(Quaternion(0.0f, -30.0f, 0.0f));
//Print(tblPlateau[indice].vars["x"]);
}
//we link the game element (Cross/Cicle normally) to it's node to be able to remove it within the game
Node@ gameElementNode = plateauParent.CreateChild("player"+activePlayer+" gameElement");
//we center the element in the dalle
gameElementNode.position = Vector3(caseX*FLOOR_ELEMENT_SIZE+(FLOOR_ELEMENT_SIZE/2), 0.2f, caseY*FLOOR_ELEMENT_SIZE+(FLOOR_ELEMENT_SIZE/2));
StaticModel@ staticModel = gameElementNode.CreateComponent("StaticModel");
log.Info("Player " + activePlayer +" have play in ["+selectedNode.vars["x"].GetUInt()+"]["+selectedNode.vars["y"].GetUInt()+"]");
//set node local vars
selectedNode.vars["playedBy"]= activePlayer ;
//easy way to know it's an end of play ^^
numCasePlayed++;
if (activePlayer==1) {
staticModel.model = cache.GetResource("Model", "Models/Sphere.mdl");
staticModel.material = cache.GetResource("Material", "Materials/Terrain.xml");
//We scale object according to the floor size
gameElementNode.SetScale(FLOOR_ELEMENT_SIZE/4.0f);
}
else {
staticModel.model = cache.GetResource("Model", "Models/Torus.mdl");
staticModel.material = cache.GetResource("Material", "Materials/Terrain.xml");
gameElementNode.SetScale(FLOOR_ELEMENT_SIZE/4.0f);
}
// Did we have a winner ?
if (didWeHaveAWinner(caseX,caseY)) {
activePlayerText.text ="player "+activePlayer +" Win!" ;
showMenu();
}
else
//all area fill & no winner
if (numCasePlayed == ROW_FLOW_NUMBER*ROW_FLOW_NUMBER) {
activePlayerText.text="End Of Game, it's a draw !";
//showMenu();
}
else {
//game continue, change current player
if (activePlayer==1)
activePlayer=2;
else
activePlayer=1;
activePlayerText.text ="player "+activePlayer;
}
}
else {
log.Info ("!! Node Already played by player "+selectedNode.vars["playedBy"].GetUInt()+"["+selectedNode.vars["x"].GetUInt()+"]["+selectedNode.vars["y"].GetUInt()+"]");
}
}
}
}
/**
* Maybe we need a menu here :p
* */
void showMenu()
{
endOfGame();
}
/**
* Detection of a winner
* */
bool didWeHaveAWinner(int x_,int y_)
{
int nbFound=0;
int i=0;
int indice=0;
// ## Horizontal Search
while (x_+i<ROW_FLOW_NUMBER && activePlayer==tblPlateau[y_*ROW_FLOW_NUMBER+x_+i].vars["playedBy"].GetUInt()) i++;
nbFound=i;i=0;
while (x_-i>=0 && activePlayer==tblPlateau[y_*ROW_FLOW_NUMBER+x_-i].vars["playedBy"].GetUInt()) i++;
nbFound+=i-1; //we doesn't count 2 time the originate cell
log.Info("Horizontale : "+nbFound+" ");
if (nbFound>=REQUIRED_TO_WIN)//have we a horizontal winner ?
return true;
// ## Vertical search
i=0;
while (y_+i<ROW_FLOW_NUMBER && activePlayer==tblPlateau[(y_+i)*ROW_FLOW_NUMBER+x_].vars["playedBy"].GetUInt()) i++;
nbFound=i;i=0;//go "Down"
while ((y_-i)>=0 && activePlayer==tblPlateau[(y_-i)*ROW_FLOW_NUMBER+x_].vars["playedBy"].GetUInt()){
//If you want to be able to trace the algo :
// indice=(y_-i)*ROW_FLOW_NUMBER+x_;
//log.Info("up "+tblPlateau[indice].name+" x: "+tblPlateau[indice].vars["x"].GetUInt()+" y: "+tblPlateau[indice].vars["y"].GetUInt()+
//" Player: "+tblPlateau[indice].vars["playedBy"].GetUInt()+" Player : "+activePlayer);
i++;
}
nbFound+=i-1;
log.Info("Verticale : "+nbFound);
if (nbFound>=REQUIRED_TO_WIN)
return true;
// ## Diagonal Search
i=0;while (y_+i<ROW_FLOW_NUMBER && x_+i<ROW_FLOW_NUMBER && activePlayer==tblPlateau[(y_+i)*ROW_FLOW_NUMBER+x_+i].vars["playedBy"].GetUInt()) i++;
nbFound=i;
i=0;while ( (y_-i)>=0 && (x_-i)>=0 && activePlayer==tblPlateau[(y_-i)*ROW_FLOW_NUMBER+x_-i].vars["playedBy"].GetUInt()) i++;
nbFound+=i-1;
log.Info("Diagonal / : "+nbFound+" ");
if (nbFound>=REQUIRED_TO_WIN)//have we a horizontal winner ?
return true;
i=0;while (y_+i<ROW_FLOW_NUMBER && x_-i>=0 && activePlayer==tblPlateau[(y_+i)*ROW_FLOW_NUMBER+x_-i].vars["playedBy"].GetUInt()) i++;
nbFound=i;
i=0;while ( (y_-i)>=0 && (x_+i)<ROW_FLOW_NUMBER && activePlayer==tblPlateau[(y_-i)*ROW_FLOW_NUMBER+x_+i].vars["playedBy"].GetUInt()) i++;
nbFound+=i-1;
log.Info("Diagonal \\ : "+nbFound+" ");
if (nbFound>=REQUIRED_TO_WIN)//have we a horizontal winner ?
return true;
return false;
}
/**
* Cast a Ray and store the 3D pos in hitPos, and the node in hitDrawable (stolen from 08_decals)
* */
bool Raycast(float maxDistance, Vector3& hitPos, Drawable@& hitDrawable)
{
hitDrawable = null;
IntVector2 pos = ui.cursorPosition;
// Check the cursor is visible and there is no UI element in front of the cursor
if (!ui.cursor.visible || ui.GetElementAt(pos, true) !is null)
return false;
Camera@ camera = cameraNode.GetComponent("Camera");
Ray cameraRay = camera.GetScreenRay(float(pos.x) / graphics.width, float(pos.y) / graphics.height);
// Pick only geometry objects, not eg. zones or lights, only get the first (closest) hit
// Note the convenience accessor to scene's Octree component
RayQueryResult result = scene_.octree.RaycastSingle(cameraRay, RAY_TRIANGLE, maxDistance, DRAWABLE_GEOMETRY);
if (result.drawable !is null) {
hitPos = result.position;
hitDrawable = result.drawable;
return true;
}
return false;
}
void SubscribeToEvents()
{
// Subscribe HandleUpdate() function for processing update events
SubscribeToEvent("Update", "HandleUpdate");
// debug geometry
SubscribeToEvent("PostRenderUpdate", "HandlePostRenderUpdate");
}
void HandleUpdate(StringHash eventType, VariantMap& eventData)
{
// Take the frame time step, which is stored as a float
float timeStep = eventData["TimeStep"].GetFloat();
// Move the camera, scale movement with time step
MoveCamera(timeStep);
}
void HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData)
{
// If draw debug mode is enabled, draw viewport debug geometry. This time use depth test, as otherwise the result becomes
// hard to interpret due to large object count
if (drawDebug)
renderer.DrawDebugGeometry(true);
}
you can change number of cell, number of object to align and size of a cell, simple look at the start of the script.
next step is to clean the code, add an IA, maybe a menu with the UI
and after a LUA / C++ version to understand more stuff.
(i know using a fine 3D engine for Urho3d is really OVERKILL, apport nothing but it’s for testing somes concepts with Urho3D