Pārlūkot izejas kodu

Update data binding sample with a cute new invaders game

Michael Ragazzon 4 gadi atpakaļ
vecāks
revīzija
aea3407a54

+ 173 - 42
Samples/basic/databinding/data/databinding.rml

@@ -107,6 +107,149 @@ p.title
 }
 
 
+/***  Invaders  ***/
+
+panel#invaders {
+	max-width: 650dp;
+}
+
+.sidebox {
+	text-align: center;
+	position: fixed;
+	top: 50dp;
+	left: 50dp;
+	width: 150dp;
+	font-size: 20dp;
+	line-height: 1.5em;
+}
+.sidebox.right {
+	left: auto;
+	right: 50dp;
+}
+
+progress {
+	display: block;
+	background: #ccc3;
+	margin-top: 8dp;
+	width: 100%;
+	box-sizing: border-box;
+	border-width: 1dp;
+}
+
+.health {
+	color: #cfc;
+	font-effect: glow(1dp #060);
+}
+.health progress {
+	border-color: #0606;
+}
+.health progress fill {
+	background: #4d4a;
+}
+.charge {
+	top: 150dp;
+	color: #dde;
+	font-effect: glow(1dp #269);
+}
+.charge progress {
+	border-color: #1486;
+}
+.charge progress fill {
+	background: #49da;
+}
+
+.time, .score {
+	color: #eec;
+	font-effect: glow(1dp #663);
+}
+.score {
+	top: 150dp;
+}
+
+.messagebox {
+	position: absolute;
+	top: 150dp;
+	left: 25%;
+	right: 25%;
+	padding: 50dp 80dp;
+	border-radius: 15dp;
+	background-color: #888c;
+	font-effect: shadow(3px 3px #333);
+}
+.messagebox button {
+	margin-top: 2em;
+}
+.start_game {
+	font-size: 20dp;
+}
+@keyframes game_over {
+	from { transform: rotate(-8deg); }
+	to   { transform: rotate( 8deg); }
+}
+.game_over {
+	font-size: 40dp;
+	animation: 0.7s elastic-in-out infinite alternate game_over;
+}
+
+invader {
+	display: inline-block;
+	position: relative;
+	width: 150dp;
+	height: 100dp;
+	background-color: #ccc3;
+	margin: 10dp 20dp;
+	border: 1dp #5553;
+	transition: opacity visibility background-color cubic-out 0.5s;
+	opacity: 0;
+	visibility: hidden;
+}
+invader.alive {
+	visibility: visible;
+	opacity: 1;
+}
+invader.firing {
+	background-color: #ee3d;
+}
+invader img {
+	display: block;
+	margin: auto auto;
+	padding-bottom: 10dp;
+	cursor: cross;
+}
+.paused invader img  {
+	cursor: arrow;
+}
+invader progress {
+	position: absolute;
+	margin: 0;
+	border-width: 0;
+	width: 10dp;
+	top: 0;
+	bottom: 0;
+	height: 100%;
+}
+.invader-health {
+	left: 0;
+}
+.invader-health fill {
+	background: #252a;
+}
+.invader-charge {
+	right: 0;
+}
+.invader-charge fill {
+	background: #d43a;
+}
+invader name {
+	display: block;
+	position: absolute;
+	color: #333;
+	left: 12dp;
+	right: 12dp;
+	bottom: 0;
+}
+
+
 /***  Forms  ***/
 
 form {
@@ -116,12 +259,6 @@ form {
 form input, form select {
 	margin-left: 0;
 }
-form input.text, form input.password {
-	width: 35%;
-}
-input.text.two-wide {
-	width: 70%;
-}
 form h2 {
 	display: block;
 	font-size: 16dp;
@@ -129,27 +266,6 @@ form h2 {
 	margin-top: 1em;
 	margin-bottom: 0.3em;
 }
-#rating {
-	display: inline-block;
-	width: 40dp;
-	padding-left: 1em;
-}
-#rating_emoji { 
-	color: #ffd40f;
-	font-size: 1.7em;
-}
-#controls textarea 
-{
-	font-size: 18dp;
-	font-effect: outline(2dp #060);
-	color: #ddd;
-	caret-color: #060;
-}
-#controls textarea selection
-{
-	background-color: #aca;
-	color: #999;
-}
 a {
 	cursor: pointer;
 }
@@ -204,28 +320,43 @@ select selectbox option.disabled {
 </panel>
 <tab>Invaders</tab>
 <panel id="invaders" data-model="invaders">
-	<p>
-		Incoming invaders:
-		<input type="range" name="rating" min="0" max="20" step="5" data-value="incoming_invaders_rate"/>
-		{{ incoming_invaders_rate }} / min.
-	</p>
-	<button data-event-click="launch_weapons">Launch weapons!</button>
-	<div data-for="invader : invaders">
-		<h1 data-class-red="invader.danger_rating > 70">{{invader.name}}</h1>
-		<p>Invader {{it_index + 1}} of {{ invaders.size }}.</p>
-		<img data-attr-sprite="invader.sprite" data-style-image-color="invader.color" data-event-click="invader.color = 'rgba(100,50,0,255)'"/>
-		<p>
-			Shots fired (damage): <span data-for="invader.damage"> {{it}} </span>
-		</p>
+	<div class="sidebox health">
+		Health
+		<progress data-attr-value="health" max="100"/>
+	</div>
+	<div class="sidebox charge">
+		Charge
+		<progress data-attr-value="charge" max="100"/>
+	</div>
+	<div class="sidebox right time">
+		Time<br/>{{ elapsed_time | format_time }}
+	</div>
+	<div class="sidebox right score">
+		Score<br/>{{ score }}
+	</div>
+	<div class="messagebox start_game" data-if="health == 0 && num_games_played == 0">
+		Do you dare to take on the fight against the alien invaders?<br/>
+		<button data-event-click="start_game">Start game</button>
+	</div>
+	<div class="messagebox game_over" data-if="health == 0 && num_games_played > 0">
+		Game Over!<br/>
+		<button data-event-click="start_game">Restart game</button>
+	</div>
+	<div class="invaders" data-class-paused="health == 0">
+		<invader data-for="invader : invaders" data-class-alive="invader.health > 0" data-class-firing="invader.charge >= 100">
+			<progress class="invader-health" direction="top" data-attr-max="invader.max_health" data-attr-value="invader.health"/>
+			<progress class="invader-charge" direction="top" data-attr-value="invader.charge" max="100"/>
+			<img data-attr-sprite="invader.sprite" data-style-image-color="invader.color" data-event-click="fire(it_index)"/>
+			<name>{{ invader.name }}</name>
+		</invader>
 	</div>
-	<h1 data-if="invaders.size == 0">It's all safe and sound, sir!</h1>
 </panel>
 <tab>Forms</tab>
 <panel id="controls" data-model="forms">
 	<form onsubmit="submit_form">
 		<h2>Rating</h2>
 		<div>
-			<input type="range" name="rating" min="0" max="100" step="1" value="50" onchange="rating" data-value="rating"/> <span id="rating">{{rating}}</span><span id="rating_emoji">&nbsp;</span>
+			<input type="range" min="0" max="100" step="1" value="50" data-value="rating"/> {{rating}}
 		</div>
 		<h2>Favorite animal: {{ animal }}</h2>
 		<div>

+ 141 - 55
Samples/basic/databinding/src/main.cpp

@@ -158,31 +158,78 @@ namespace InvadersExample {
 
 	Rml::DataModelHandle model_handle;
 
+	static constexpr int num_invaders = 12;
+	static constexpr double incoming_invaders_rate = 50; // Per minute
+
 	struct Invader {
 		Rml::String name;
 		Rml::String sprite;
 		Rml::Colourb color{ 255, 255, 255 };
-		Rml::Vector<int> damage;
-		float danger_rating = 50;
+		float max_health = 0;
+		float charge_rate = 0;
+		float health = 0;
+		float charge = 0;
 	};
 
 	struct InvadersData {
-		double time_last_invader_spawn = 0;
-		double time_last_weapons_launched = 0;
+		float health = 0;
+		float charge = 0;
+		int score = 0;
+		
+		double elapsed_time = 0;
+		double next_invader_spawn_time = 0;
 
-		float incoming_invaders_rate = 10; // Per minute
+		int num_games_played = 0;
 
-		Rml::Vector<Invader> invaders = {
-			Invader{"Angry invader", "icon-invader", {255, 40, 30}, {3, 6, 7}, 80}
-		};
+		Rml::Array<Invader, num_invaders> invaders;
 
-		void LaunchWeapons(Rml::DataModelHandle model, Rml::Event& /*ev*/, const Rml::VariantList& /*arguments*/)
+		// Start a new game.
+		void StartGame(Rml::DataModelHandle model, Rml::Event& /*ev*/, const Rml::VariantList& /*arguments*/)
 		{
-			invaders.clear();
+			health = 100;
+			charge = 30;
+			score = 0;
+			elapsed_time = 0;
+			next_invader_spawn_time = 0;
+			num_games_played += 1;
+
+			for (Invader& invader : invaders)
+				invader.health = 0;
+
+			model.DirtyVariable("health");
+			model.DirtyVariable("charge");
+			model.DirtyVariable("score");
+			model.DirtyVariable("elapsed_time");
+			model.DirtyVariable("num_games_played");
 			model.DirtyVariable("invaders");
 		}
 
-	} invaders_data;
+		// Fire on the invader of the given index (first argument).
+		void Fire(Rml::DataModelHandle model, Rml::Event& /*ev*/, const Rml::VariantList& arguments)
+		{
+			if (arguments.size() != 1)
+				return;
+			const std::size_t index = arguments[0].Get<std::size_t>();
+			if (index >= invaders.size())
+				return;
+
+			Invader& invader = invaders[index];
+			if (health <= 0 || invader.health <= 0)
+				return;
+
+			const float new_health = Rml::Math::Max(invader.health - charge * Rml::Math::SquareRoot(charge), 0.0f);
+
+			charge = 30.f;
+			score += int(invader.health - new_health) + 1000 * (new_health == 0);
+
+			invader.health = new_health;
+
+			model.DirtyVariable("invaders");
+			model.DirtyVariable("charge");
+			model.DirtyVariable("score");
+		}
+
+	} data;
 
 	bool Initialize(Rml::Context* context)
 	{
@@ -190,86 +237,122 @@ namespace InvadersExample {
 		if (!constructor)
 			return false;
 
-		// Register a custom getter/setter for the Colourb type.
+		// Register a custom getter for the Colourb type.
 		constructor.RegisterScalar<Rml::Colourb>(
 			[](const Rml::Colourb& color, Rml::Variant& variant) {
 				variant = "rgba(" + Rml::ToString(color) + ')';
-			},
-			[](Rml::Colourb& color, const Rml::Variant& variant) {
-				Rml::String str = variant.Get<Rml::String>();
-				bool success = false;
-				if (str.size() > 6 && str.substr(0, 5) == "rgba(")
-					success = Rml::TypeConverter<Rml::String, Rml::Colourb>::Convert(str.substr(5), color);
-				if (!success)
-					Rml::Log::Message(Rml::Log::LT_WARNING, "Invalid color specified: '%s'. Use syntax rgba(R,G,B,A).", str.c_str());
 			}
 		);
+		// Register a transform function for formatting time
+		constructor.RegisterTransformFunc("format_time", [](Rml::Variant& variant, const Rml::VariantList& /*arguments*/) -> bool {
+			const double t = variant.Get<double>();
+			const int minutes = int(t) / 60;
+			const double seconds = t - 60.0 * double(minutes);
+			variant = Rml::CreateString(10, "%02d:%05.2f", minutes, seconds);
+			return true;
+		});
 
-		// Since Invader::damage is an array type.
-		constructor.RegisterArray<Rml::Vector<int>>();
-
-		// Structs are registered by adding all its members through the returned handle.
+		// Structs are registered by adding all their members through the returned handle.
 		if (auto invader_handle = constructor.RegisterStruct<Invader>())
 		{
 			invader_handle.RegisterMember("name", &Invader::name);
 			invader_handle.RegisterMember("sprite", &Invader::sprite);
 			invader_handle.RegisterMember("color", &Invader::color);
-			invader_handle.RegisterMember("damage", &Invader::damage);
-			invader_handle.RegisterMember("danger_rating", &Invader::danger_rating);
+			invader_handle.RegisterMember("max_health", &Invader::max_health);
+			invader_handle.RegisterMember("charge_rate", &Invader::charge_rate);
+			invader_handle.RegisterMember("health", &Invader::health);
+			invader_handle.RegisterMember("charge", &Invader::charge);
 		}
 
 		// We can even have an Array of Structs, infinitely nested if we so desire.
 		// Make sure the underlying type (here Invader) is registered before the array.
-		constructor.RegisterArray<Rml::Vector<Invader>>();
+		constructor.RegisterArray<decltype(data.invaders)>();
 
 		// Now we can bind the variables to the model.
-		constructor.Bind("incoming_invaders_rate", &invaders_data.incoming_invaders_rate);
-		constructor.Bind("invaders", &invaders_data.invaders);
-
-		// This function will be called when the user clicks the 'Launch weapons' button.
-		constructor.BindEventCallback("launch_weapons", &InvadersData::LaunchWeapons, &invaders_data);
+		constructor.Bind("invaders", &data.invaders);
+		constructor.Bind("health", &data.health);
+		constructor.Bind("charge", &data.charge);
+		constructor.Bind("score", &data.score);
+		constructor.Bind("elapsed_time", &data.elapsed_time);
+		constructor.Bind("num_games_played", &data.num_games_played);
+
+		// This function will be called when the user clicks on the (re)start game button.
+		constructor.BindEventCallback("start_game", &InvadersData::StartGame, &data);
+		// This function will be called when the user clicks on any of the invaders.
+		constructor.BindEventCallback("fire", &InvadersData::Fire, &data);
 
 		model_handle = constructor.GetModelHandle();
 
 		return true;
 	}
 
-	void Update(const double t)
+	void Update(const double dt)
 	{
-		// Add new invaders at regular time intervals.
-		const double t_next_spawn = invaders_data.time_last_invader_spawn + 60.0 / double(invaders_data.incoming_invaders_rate);
-		if (t >= t_next_spawn)
+		using namespace Rml;
+		
+		if (data.health == 0)
+			return;
+
+		data.elapsed_time += dt;
+		model_handle.DirtyVariable("elapsed_time");
+
+		// Steadily increase the player charge.
+		data.charge = Math::Min(data.charge + float(40.0 * dt), 100.f);
+		model_handle.DirtyVariable("charge");
+
+		// Add new invaders at the scheduled time.
+		if (data.elapsed_time >= data.next_invader_spawn_time)
 		{
-			using namespace Rml;
-			const int num_items = 4;
+			constexpr int num_items = 4;
 			static Array<String, num_items> names = { "Angry invader", "Harmless invader", "Deceitful invader", "Cute invader" };
 			static Array<String, num_items> sprites = { "icon-invader", "icon-flag", "icon-game", "icon-waves" };
 			static Array<Colourb, num_items> colors = { { { 255, 40, 30 }, {20, 40, 255}, {255, 255, 30}, {230, 230, 230} } };
 
 			Invader new_invader;
-			new_invader.name = names[rand() % num_items];
-			new_invader.sprite = sprites[rand() % num_items];
-			new_invader.color = colors[rand() % num_items];
-			new_invader.danger_rating = float((rand() % 100) + 1);
-			invaders_data.invaders.push_back(new_invader);
-
-			model_handle.DirtyVariable("invaders");
-			invaders_data.time_last_invader_spawn = t;
+			new_invader.name = names[Math::RandomInteger(num_items)];
+			new_invader.sprite = sprites[Math::RandomInteger(num_items)];
+			new_invader.color = colors[Math::RandomInteger(num_items)];
+
+			new_invader.max_health = 300.f + float(30.0 * data.elapsed_time) + Math::RandomReal(300.f);
+			new_invader.charge_rate = 10.f + Math::RandomReal(50.f);
+
+			new_invader.health = new_invader.max_health;
+
+			// Find an available slot to spawn the new invader in.
+			const int i_begin = Math::RandomInteger(num_invaders);
+			for (int i = 0; i < num_invaders; i++)
+			{
+				Invader& invader = data.invaders[(i + i_begin) % num_invaders];
+				if (invader.health <= 0)
+				{
+					invader = std::move(new_invader);
+					model_handle.DirtyVariable("invaders");
+					break;
+				}
+			}
+
+			// Add new invaders at steadily decreasing time intervals.
+			data.next_invader_spawn_time = data.elapsed_time + 60.0 / (incoming_invaders_rate + 0.1 * data.elapsed_time);
 		}
 
-		// Launch shots from a random invader.
-		if (t >= invaders_data.time_last_weapons_launched + 1.0)
+		// Iterate through the invaders and fire at the player.
+		for (Invader& invader : data.invaders)
 		{
-			if (!invaders_data.invaders.empty())
+			if (invader.health > 0)
 			{
-				const size_t index = size_t(rand() % int(invaders_data.invaders.size()));
+				invader.charge = invader.charge + invader.charge_rate * float(dt);
 
-				Invader& invader = invaders_data.invaders[index];
-				invader.damage.push_back(rand() % int(invader.danger_rating));
+				if (invader.charge >= 100)
+				{
+					data.health = Math::Max(data.health - float(10.0 * dt), 0.0f);
+					model_handle.DirtyVariable("health");
+				}
+				
+				if (invader.charge >= 120)
+					invader.charge = 0;
 
 				model_handle.DirtyVariable("invaders");
 			}
-			invaders_data.time_last_weapons_launched = t;
 		}
 	}
 }
@@ -390,10 +473,13 @@ ShellRenderInterfaceExtensions *shell_renderer;
 
 void GameLoop()
 {
+	static double t_prev = 0;
 	const double t = Rml::GetSystemInterface()->GetElapsedTime();
-	
+	const double dt = Rml::Math::Min(t - t_prev, 0.1);
+	t_prev = t;
+
 	EventsExample::Update();
-	InvadersExample::Update(t);
+	InvadersExample::Update(dt);
 
 	context->Update();