OBJECTIVES
The main objective is to create the classic videogame "Pong". Our Pong will have a simple menu scene and game scene.We will learn to create layer screens to navigate them, entities that works like buttons, play sounds, we will create a simple AI, a simple player controller and other things that we will see here.
CREATE A NEW PROJECT
Create a new Game Project from the Wave Engine template, and name it Pong:
ADDING RESOURCES
The first thing is to add all assets that we will need in our game. We can download these assets here. We need a player's sprite, a background, a ball, a win image and walls. We also provided a Font and two sounds for playing with goal and collision.
Note: You can learn the way to export all assets in first tutorials with Assets Exporter.We encourage you to separate assets depending on their type. CREATE MENU SCENE
Rename the class MyScene.cs with MenuScene.cs. In CreateScene() delete the Simple test and add this code to add the tittle:
EntityManager.Add(camera2D);
int offset = 100;
var title = new Entity("Title")
.AddComponent(new Sprite("Content/Texture/TitlePong.wpk"))
.AddComponent(new SpriteRenderer(DefaultLayers.Alpha))
.AddComponent(new Transform2D()
{
Y = WaveServices.Platform.ScreenHeight / 2 - offset,
X = WaveServices.Platform.ScreenWidth / 2 - 150
});
EntityManager.Add(title);
Don't forget include the next usings that we will need in this class:
using WaveEngine.Components.Gestures;
using WaveEngine.Components.Graphics2D;
using WaveEngine.Components.UI;
using WaveEngine.Framework;
using WaveEngine.Framework.Graphics;
using WaveEngine.Framework.Physics2D;
using WaveEngine.Framework.Services;
After that we will create two entities, both will be buttons to enter the game, in multiplayer version or single player version:
.AddComponent(new Transform2D()
{
Y = WaveServices.Platform.ScreenHeight / 2 + 50,
X = WaveServices.Platform.ScreenWidth / 2 - offset,
XScale = 2f,
YScale = 2f
})
.AddComponent(new TextControl()
{
Text = "Multiplayer",
Foreground = Color.White,
})
.AddComponent(new TextControlRenderer())
.AddComponent(new RectangleCollider())
.AddComponent(new TouchGestures());
multiplayerButtonEntity.FindComponent<TouchGestures>().TouchPressed += new EventHandler<GestureEventArgs>(Multiplayer_TouchPressed);
EntityManager.Add(multiplayerButtonEntity);
var singleplayerButtonEntity = new Entity("SingleplayerButton")
.AddComponent(new Transform2D()
{
Y = WaveServices.Platform.ScreenHeight / 2,
X = WaveServices.Platform.ScreenWidth / 2 - offset,
XScale = 2f,
YScale = 2f
})
.AddComponent(new TextControl()
{
Text = "Single Player",
Foreground = Color.White,
})
.AddComponent(new TextControlRenderer())
.AddComponent(new RectangleCollider())
.AddComponent(new TouchGestures());
singleplayerButtonEntity.FindComponent<TouchGestures>().TouchPressed += new EventHandler<GestureEventArgs>(Singleplayer_TouchPressed);
EntityManager.Add(singleplayerButtonEntity);
Now add the two event handlers, we will use to change of scenes:
{
ScreenContext screenContext = new ScreenContext(new GameScene())
{
Name = "FromMultiplayer",
};
WaveServices.ScreenContextManager.To(screenContext);
}
private void Singleplayer_TouchPressed(object sender, GestureEventArgs e)
{
ScreenContext screenContext = new ScreenContext(new GameScene())
{
Name = "FromSingleplayer",
};
WaveServices.ScreenContextManager.To(screenContext);
}
If we build and run, we will see:
Now, we create a class, this class will be GameScene.cs, it is where the game occurs.If we click in any entity text, the scene will change to game scene, but this scene is empty now.
CREATE GAME SCENE
The first thing that we do is to create the background in the CreateScene method:
EntityManager.Add(camera2D);
int offsetTop = 50;
//Create BackGround
var background = new Entity("Background")
.AddComponent(new Sprite("Content/Texture/real-pong.wpk"))
.AddComponent(new SpriteRenderer(DefaultLayers.Alpha))
.AddComponent(new Transform2D()
{
XScale = 1.5f,
YScale = 1.45f,
});
Don’t forget include the next usings that we will need in this class:
using WaveEngine.Common.Math;
using WaveEngine.Components.Gestures;
using WaveEngine.Components.Graphics2D;
using WaveEngine.Components.UI;
using WaveEngine.Framework;
using WaveEngine.Framework.Graphics;
using WaveEngine.Framework.Managers;
using WaveEngine.Framework.Physics2D;
using WaveEngine.Framework.Services;
using WaveEngine.Framework.Sound;
using WaveEngine.Framework.UI;
We will see:
And now add the walls:
var barTop = new Entity("BarTop")
.AddComponent(new Sprite("Content/Texture/wall.wpk"))
.AddComponent(new SpriteRenderer(DefaultLayers.Alpha))
.AddComponent(new RectangleCollider())
.AddComponent(new Transform2D()
{
XScale = 1.55f,
YScale = 2f,
});
var barBot = new Entity("BarBot")
.AddComponent(new Sprite("Content/Texture/wall.wpk"))
.AddComponent(new SpriteRenderer(DefaultLayers.Alpha))
.AddComponent(new RectangleCollider())
.AddComponent(new Transform2D()
{
XScale = 1.55f,
YScale = 2f,
Y = WaveServices.Platform.ScreenHeight - 25,
});
//Add entities
EntityManager.Add(barTop);
EntityManager.Add(barBot);
.AddComponent(new Sprite("Content/Texture/player.wpk"))
.AddComponent(new SpriteRenderer(DefaultLayers.Alpha))
.AddComponent(new RectangleCollider())
.AddComponent(new Transform2D()
{
Origin = new Vector2(0.5f, 1),
X = WaveServices.Platform.ScreenWidth / 50,
Y = WaveServices.Platform.ScreenHeight / 2
})
.AddComponent(new PlayerBehavior());
//Add entities
EntityManager.Add(player);
Now, we will create the behavior to move the player in the scene. We need to create a new class, and rename it with the name PlayerBehavior.cs.This is our class:
{
private const int SPEED = 5;
private const int UP = -1;
private const int DOWN = 1;
private const int NONE = 0;
private const int BORDER_OFFSET = 25;
[RequiredComponent]
public Transform2D trans2D;
/// <summary>
/// 1 or -1 indicating up or down respectively
/// </summary>
private int direction;
private PlayerState currentState, lastState;
private enum PlayerState { Idle, Up, Down };
public PlayerBehavior()
: base("PlayerBehavior")
{
this.direction = NONE;
this.trans2D = null;
this.currentState = PlayerState.Idle;
}
protected override void Update(TimeSpan gameTime)
{
currentState = PlayerState.Idle;
// Keyboard
var keyboard = WaveServices.Input.KeyboardState;
if (keyboard.W == ButtonState.Pressed)
{
currentState = PlayerState.Up;
}
else if (keyboard.S == ButtonState.Pressed)
{
currentState = PlayerState.Down;
}
// Set current state if that one is diferent
if (currentState != lastState)
{
switch (currentState)
{
case PlayerState.Idle:
direction = NONE;
break;
case PlayerState.Up:
direction = UP;
break;
case PlayerState.Down:
direction = DOWN;
break;
}
}
lastState = currentState;
// Move sprite
trans2D.Y += direction * SPEED * (gameTime.Milliseconds / 10);
// Check borders
if (trans2D.Y < BORDER_OFFSET + trans2D.YScale + 80)
{
trans2D.Y = BORDER_OFFSET + trans2D.YScale + 80;
}
else if (trans2D.Y > WaveServices.Platform.ScreenHeight - BORDER_OFFSET)
{
trans2D.Y = WaveServices.Platform.ScreenHeight - BORDER_OFFSET;
}
}
}
and these are the usings that we will need:
using WaveEngine.Framework;
using WaveEngine.Framework.Graphics;
using WaveEngine.Framework.Services;
Now our player can move in our game scene with the keys “w” to move up and “s” to move down.Come back to GameScene.cs and add the second player:
.AddComponent(new Sprite("Content/Texture/player.wpk"))
.AddComponent(new SpriteRenderer(DefaultLayers.Alpha))
.AddComponent(new RectangleCollider())
.AddComponent(new Transform2D()
{
Origin = new Vector2(0.5f, 1),
X = WaveServices.Platform.ScreenWidth - 15,
Y = WaveServices.Platform.ScreenHeight / 2
});
//Add entities
EntityManager.Add(player2);
If we build and run we will see:
Now we will create the ball:
var ball = new Entity("Ball")
.AddComponent(new Sprite("Content/Texture/ball.wpk"))
.AddComponent(new SpriteRenderer(DefaultLayers.Alpha))
.AddComponent(new RectangleCollider())
.AddComponent(new Transform2D()
{
Origin = new Vector2(0.5f, 1),
X = WaveServices.Platform.ScreenWidth / 2,
Y = WaveServices.Platform.ScreenHeight / 2
})
.AddComponent(new BallBehavior(player, barBot, barTop, player2));
//Add entities
EntityManager.Add(ball);
And the behavior to move the ball. We need to create a new class, and rename it BallBehavior.cs. And add the following code:
{
private const float SPEED = 4f;
private const int BORDER_OFFSET = 20;
private Entity player;
private RectangleCollider rectPlayer;
private Entity player2;
private RectangleCollider rectPlayer2;
private Entity barBot;
private RectangleCollider rectBarBot;
private Entity barTop;
private RectangleCollider rectBarTop;
private int verticalDirection = -1;
private int horizontalDirection = -1;
private float speed = SPEED;
private float incrementSpeed = 0.5f;
private int goals1 = 0;
private int goals2 = 0;
private bool checkGoal = false;
[RequiredComponent]
public Transform2D trans2D;
public int Goal1 { get { return goals1; } private set { goals1 = value; } }
public int Goal2 { get { return goals2; } private set { goals2 = value; } }
public int HorizontalDirection { get {return horizontalDirection; } }
public BallBehavior(Entity player, Entity barBot, Entity barTop, Entity playerIA)
: base("BallBehavior")
{
this.trans2D = null;
this.player = player;
this.rectPlayer = player.FindComponent<RectangleCollider>();
this.player2 = playerIA;
this.rectPlayer2 = playerIA.FindComponent<RectangleCollider>();
this.barBot = barBot;
this.rectBarBot = barBot.FindComponent<RectangleCollider>();
this.barTop = barTop;
this.rectBarTop = barTop.FindComponent<RectangleCollider>();
}
protected override void Update(TimeSpan gameTime)
{
//Check Goals
if (trans2D.X <= 0 && !checkGoal)
{
(Owner.Scene as GameScene).CurrentState = GameScene.State.Goal;
checkGoal = true;
goals2++;
StartBall();
}
if (trans2D.X >= WaveServices.Platform.ScreenWidth && !checkGoal)
{
(Owner.Scene as GameScene).CurrentState = GameScene.State.Goal;
checkGoal = true;
goals1++;
StartBall();
}
//Move Ball
if (trans2D.X > 0 && trans2D.X < WaveServices.Platform.ScreenWidth)
{
trans2D.X += horizontalDirection * speed * (gameTime.Milliseconds / 10);
trans2D.Y += verticalDirection * speed * (gameTime.Milliseconds / 10);
}
// Check collisions
if (rectPlayer.Contain(new Vector2(trans2D.X, trans2D.Y)))
{
horizontalDirection = 1;
speed += incrementSpeed;
(Owner.Scene as GameScene).PlaySoundCollision();
}
if (rectPlayer2.Contain(new Vector2(trans2D.X, trans2D.Y)))
{
horizontalDirection = -1;
speed += incrementSpeed;
(Owner.Scene as GameScene).PlaySoundCollision();
}
if (rectBarBot.Contain(new Vector2(trans2D.X, trans2D.Y)))
{
verticalDirection = -1;
(Owner.Scene as GameScene).PlaySoundCollision();
}
if (rectBarTop.Contain(new Vector2(trans2D.X, trans2D.Y - 15)))
{
verticalDirection = 1;
(Owner.Scene as GameScene).PlaySoundCollision();
}
}
//Start new ball
public void StartBall()
{
trans2D.X = WaveServices.Platform.ScreenWidth / 2;
trans2D.Y = WaveServices.Platform.ScreenHeight / 2;
checkGoal = false;
speed = SPEED;
}
}
these are the usings of BallBehavior.cs:
using WaveEngine.Framework;
using WaveEngine.Framework.Graphics;
using WaveEngine.Framework.Physics2D;
using WaveEngine.Framework.Services;
With this we need to add a method and an attribute in the Game scene:
private SoundInfo soundPong;
public void PlaySoundCollision()
{
WaveServices.SoundPlayer.Play(soundPong);
}
To move the second player we need to add a new behavior, like the player behavior, but with other controls. We create another class, Player2Behavior.cs. It is like PlayerBehavior.cs so you can copy and paste the code, and we need to change the keys “w” and “s” for “up” and “down”.
var keyboard = WaveServices.Input.KeyboardState;
if (keyboard.Up == ButtonState.Pressed)
{
currentState = PlayerState.Up;
}
else if (keyboard.Down == ButtonState.Pressed)
{
currentState = PlayerState.Down;
}
Now, we can play with two players. We only need to add component to player2. But first we add a new class, PlayerAIBehavior.cs and add this code:
{
private const float SPEED = 5f;
private const int BORDER_OFFSET = 25;
private int direction;
private bool move = false;
[RequiredComponent]
public Transform2D trans2D;
public Entity ball;
public Transform2D transBall2D;
public BallBehavior ballBehavior;
public PlayerAIBehavior(Entity ball)
: base("PlayerIABehavior")
{
this.trans2D = null;
this.ball = ball;
this.transBall2D = ball.FindComponent<Transform2D>();
this.ballBehavior = ball.FindComponent<BallBehavior>();
this.direction = ballBehavior.HorizontalDirection;
}
protected override void Update(TimeSpan gameTime)
{
this.direction = ballBehavior.HorizontalDirection;
// Move sprite
if (this.direction > 0 && (Owner.Scene as GameScene).CurrentState == GameScene.State.Game)
move = true;
else
move = false;
if (trans2D.Y < transBall2D.Y && move)
trans2D.Y += SPEED * (gameTime.Milliseconds / 10);
else if (trans2D.Y > transBall2D.Y && move)
trans2D.Y -= SPEED * (gameTime.Milliseconds / 10);
// Check borders
if (trans2D.Y < BORDER_OFFSET + trans2D.YScale + 80)
{
trans2D.Y = BORDER_OFFSET + trans2D.YScale + 80;
}
else if (trans2D.Y > WaveServices.Platform.ScreenHeight - BORDER_OFFSET)
{
trans2D.Y = WaveServices.Platform.ScreenHeight - BORDER_OFFSET;
}
}
}
And we add the next usings for this class:
using WaveEngine.Framework.Graphics;
using WaveEngine.Framework.Services;
To start playing with two players or Multiplayer, we need to add the following code in the CreateScene method of GameScene.cs:
if (WaveServices.ScreenContextManager.CurrentContext.Name == "FromSingleplayer")
player2.AddComponent(new PlayerAIBehavior(ball));
else if (WaveServices.ScreenContextManager.CurrentContext.Name == "FromMultiplayer")
player2.AddComponent(new Player2Behavior());
Now, when we select any option to play in the menu scene, in the Game scene we will add a component to play with this option.The next thing we will be creating is the UI, we need to add some text block in the CreateScene method, but before that we need to create the variables, and some more code that is needed:
private SoundInfo soundPong;
private SoundInfo soundGoal;
//Score and Initial TextBlocks
public TextBlock textblockGoal1, textblockGoal2, textblockInit;
//Create Trophy
public Entity trophy = new Entity("Trophy");
//Game States
public enum State
{
Init,
Game,
Goal,
Win
}
public State CurrentState = State.Init;
Add the next code in the CreateScene method:
textblockGoal1 = new TextBlock("Goal1")
{
Margin = new Thickness((WaveServices.Platform.ScreenWidth / 2f) - 100, offsetTop, 0, 0),
FontPath = "Content/Font/Calisto MT.wpk",
Text = "0",
Height = 130,
};
textblockGoal2 = new TextBlock("Goal2")
{
Margin = new Thickness((WaveServices.Platform.ScreenWidth / 2f) + 50, offsetTop, 0, 0),
FontPath = "Content/Font/Calisto MT.wpk",
Text = "0",
Height = 130,
};
textblockInit = new TextBlock("TextInit")
{
Margin = new Thickness((WaveServices.Platform.ScreenWidth / 2f) - 50, offsetTop + 150, 0, 0),
FontPath = "Content/Font/Calisto MT.wpk",
Text = "",
Height = 130,
IsVisible = false,
//Trophy components
trophy.AddComponent(new Sprite("Content/Texture/trophy.wpk"));
trophy.AddComponent(new SpriteRenderer(DefaultLayers.Alpha));
trophy.AddComponent(new Transform2D());
trophy.IsVisible = false;
//Create a sound bank and sound info for game
SoundBank bank = new SoundBank(Assets);
WaveServices.SoundPlayer.RegisterSoundBank(bank);
soundPong = new SoundInfo("Content/Sound/SoundPong.wpk");
bank.Add(soundPong);
soundGoal = new SoundInfo("Content/Sound/SoundGol.wpk");
bank.Add(soundGoal);
Finally we need to create a new class, this class will be called MySceneBehavior.cs, and will control all game states:
{
private const int GOALS_TO_WIN = 2;
private int time = 3;
Entity ball;
Entity bg;
BallBehavior ballBehavior;
Timer timer;
public MySceneBehavior()
: base("PongBehavior")
{ }
protected override void Update(TimeSpan gameTime)
{
var state = (this.Scene as GameScene).CurrentState;
switch (state)
{
case GameScene.State.Init:
var textBlock = (this.Scene as GameScene).textblockInit;
textBlock.IsVisible = true;
textBlock.Text = time.ToString();
ball.IsActive = false;
bg.IsVisible = false;
if (timer == null)
{
timer = WaveServices.TimerFactory.CreateTimer("Init", TimeSpan.FromSeconds(1f), () =>
{
textBlock.Text = time.ToString();
time--;
});
}
if (time <= 0)
{
time = 3;
WaveServices.TimerFactory.RemoveTimer("Init");
timer = null;
textBlock.IsVisible = false;
bg.IsVisible = true;
SetState(GameScene.State.Game);
}
break;
case GameScene.State.Game:
ball.IsActive = true;
break;
case GameScene.State.Goal:
(this.Scene as GameScene).PlaySoundGoal();
ball.IsActive = false;
var textBlock1 = (this.Scene as GameScene).textblockGoal1;
textBlock1.Text = ballBehavior.Goal1.ToString();
var textBlock2 = (this.Scene as GameScene).textblockGoal2;
textBlock2.Text = ballBehavior.Goal2.ToString();
if (ballBehavior.Goal1 == GOALS_TO_WIN || ballBehavior.Goal2 == GOALS_TO_WIN)
{
SetState(GameScene.State.Win);
break;
}
SetState(GameScene.State.Init);
break;
case GameScene.State.Win:
(this.Scene as GameScene).trophy.IsVisible = true;
ball.IsActive = false;
if (ballBehavior.Goal1 == GOALS_TO_WIN)
{
(this.Scene as GameScene).trophy.FindComponent<Transform2D>().X = WaveServices.Platform.ScreenWidth / 2 - 300;
(this.Scene as GameScene).trophy.FindComponent<Transform2D>().Y = WaveServices.Platform.ScreenHeight / 2 - 100;
}
else
{
(this.Scene as GameScene).trophy.FindComponent<Transform2D>().X = WaveServices.Platform.ScreenWidth / 2 + 100;
(this.Scene as GameScene).trophy.FindComponent<Transform2D>().Y = WaveServices.Platform.ScreenHeight / 2 - 100;
}
break;
}
}
public void SetState(GameScene.State _State)
{
(this.Scene as GameScene).CurrentState = _State;
}
protected override void ResolveDependencies()
{
ball = (this.Scene as GameScene).EntityManager.Find<Entity>("Ball");
bg = (this.Scene as GameScene).EntityManager.Find<Entity>("Background");
ballBehavior = ball.FindComponent<BallBehavior>();
}
}
These are the usings for MySceneBehavior.cs:
using WaveEngine.Framework.Graphics;
using WaveEngine.Framework.Services;
To connect the game to come back to the menu, we need to add a back button and add the scene behavior to control the game in the create scene method:
var backButtonEntity = new Entity("BackButton")
.AddComponent(new Transform2D())
.AddComponent(new TextControl()
{
Text = "Back",
HorizontalAlignment = WaveEngine.Framework.UI.HorizontalAlignment.Center,
VerticalAlignment = WaveEngine.Framework.UI.VerticalAlignment.Bottom,
Foreground = Color.Black,
})
.AddComponent(new TextControlRenderer())
.AddComponent(new RectangleCollider())
.AddComponent(new TouchGestures());
backButtonEntity.FindComponent<TouchGestures>().TouchPressed += new EventHandler<GestureEventArgs>(Back_TouchPressed);
//Add Scene Behavior Post Update
AddSceneBehavior(new MySceneBehavior(), SceneBehavior.Order.PostUpdate);
And add a method event to the back button and play sound:
private void Back_TouchPressed(object sender, GestureEventArgs e)
{
WaveServices.TimerFactory.RemoveTimer("Init");
ScreenContext screenContext = new ScreenContext(new MenuScene())
{
Name = "FromGame",
};
WaveServices.ScreenContextManager.To(screenContext);
}
public void PlaySoundCollision()
{
WaveServices.SoundPlayer.Play(soundPong);
}
public void PlaySoundGoal()
{
WaveServices.SoundPlayer.Play(soundGoal);
}
We will first see the initial countdown: And the next image is the final result of the game:
And the next image is the final result of the game:
DONWLOAD SOURCE CODE
You can download the full project here.
Tutorial created by Carlos Sánchez López.
next time use formating code !!! pls!
I am a Wave Engine n00b, but I was working through this tutorial got hung up at:
.AddComponent(new RectangleCollider())
RectangleCollider() apparently isn't a thing. I had to change it to:
.AddComponent(new RectangleCollider2D())
to get the tutorial to work. I hope that helps someone else.