tag:blogger.com,1999:blog-25131758932624584652024-03-13T19:25:59.119-07:00ZomBPir8NinjaZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.comBlogger18125tag:blogger.com,1999:blog-2513175893262458465.post-24451016580443492012015-12-07T10:40:00.001-08:002015-12-07T10:40:35.531-08:00Enemy Detection During a Custom MoveTo for Vehicles in Unreal Engine 4Now that our vehicles can <a href="http://zombpir8ninja.blogspot.com/2015/11/collision-avoidance-for-vehicles-in.html">move themselves autonomously fairly well</a>, we can have them check for enemies nearby during their travel.<br />
<br />
Unreal 4 includes a new <a href="https://docs.unrealengine.com/latest/INT/Engine/AI/EnvironmentQuerySystem/index.html">Environment Query System</a>, which will do most of the work for us:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://docs.unrealengine.com/latest/INT/Engine/AI/EnvironmentQuerySystem/UserGuide/index.html"><img border="0" src="https://docs.unrealengine.com/latest/images/Engine/AI/EnvironmentQuerySystem/UserGuide/runEQS.jpg" /></a></div>
<div style="text-align: center;">
<i>Like FindPathToLocation taking all the fun out of PathFinding...</i></div>
<br />
The <a href="https://docs.unrealengine.com/latest/INT/Engine/AI/EnvironmentQuerySystem/QuickStart/index.html">EQS Quick Start Guide</a> does a fairly good job of walking through a sample setup, and we'll follow much of that for the first query.<br />
<br />
<b>Detecting Enemies</b><br />
<br />
A query needs three objects: a context, a generator and tests. The context represents "who": the perspective from which actor to perform the query. The generator provides "what" or "where": the list of actors or locations to choose from. Tests answer "why" items should be included or excluded from the list.<br />
<br />
The default context is the "Querier": the actor performing the query. We want to find enemies near the vehicle making the query, so no changes there.<br />
<br />
Unreal provides several built in generators, and the closest to our needs is "Actors of Class". However, that could also detect vehicles that are our allies so we need to build our own generator from EnvQueryGenerator_BlueprintBase. We want to select from all vehicles, then filter out the vehicles that are our allies.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj_X4ukbnk4sTCkTka8vJbP8Oafpq2PEFWOCoMr0ws2nAbicPJKCAlvGWduptc5N1CxKvdl_TmY2WwzURjijWNldcUk8n3SBG3eLVqZvyOLa8-K9vc_YqU84nIcj3sGOs2UiZSiXgqpyEn6/s1600/EnemyDetection1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="113" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj_X4ukbnk4sTCkTka8vJbP8Oafpq2PEFWOCoMr0ws2nAbicPJKCAlvGWduptc5N1CxKvdl_TmY2WwzURjijWNldcUk8n3SBG3eLVqZvyOLa8-K9vc_YqU84nIcj3sGOs2UiZSiXgqpyEn6/s320/EnemyDetection1.png" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<i>Clipped for readability...</i></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
Finally, we'll include the built-in tests: Distance and Trace on Visibility (to make sure it's not behind unreachable terrain).</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
The final query:</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLAMPJdVDbzXOCMUPZ3pWmrXJ8WGhaohBqJqZwxSPI9XAN_grjBpoAET0fh3KvUEdoYMOe0tUE7evr_M6CwiI6uEyQuRX-_RinuAPvquTtc09-tV7VNkp5sfiwjeacJOqw8voTG6a7Yy94/s1600/EnemyDetection2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="239" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLAMPJdVDbzXOCMUPZ3pWmrXJ8WGhaohBqJqZwxSPI9XAN_grjBpoAET0fh3KvUEdoYMOe0tUE7evr_M6CwiI6uEyQuRX-_RinuAPvquTtc09-tV7VNkp5sfiwjeacJOqw8voTG6a7Yy94/s320/EnemyDetection2.png" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
Note that we're providing a parameter (FindEnemyRadius) for the Distance query. Also, don't be fooled by "prefer greater". You can configure the test to use Inverse Linear scoring so that nearby objects score higher.</div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
The EQS provides a handy <a href="https://docs.unrealengine.com/latest/INT/Engine/AI/EnvironmentQuerySystem/EQSPawn/index.html">Testing Pawn</a> to check the results of the query during <a href="https://docs.unrealengine.com/latest/INT/Engine/UI/LevelEditor/InEditorTesting/index.html#simulateineditor">simulation</a>. It can show the scoring values as well as which tests failed on excluded items.<br />
<br />
<b>Calculating Controls to Aim</b><br />
<br />
Assuming that our vehicle has front-mounted weapons, we need to tell it how to aim at a detected enemy. Let's also assume that various vehicles may have short, medium or long-range weapons and will prefer different optimal ranges. Our logic for aim is then:<br />
<br />
<ol>
<li><a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/Collision/LineTraceByChannel/index.html">LineTraceByChannel</a> from our vehicle to the enemy to determine if we can see our target (A defeated enemy or even our ally could be in the way.)</li>
<li>If we can't see our target, drive to the LineOfFireLocation (More on that in a moment.)</li>
<li>LineTraceByChannel from our forward direction to FindEnemyRadius to check if we're ready to fire</li>
<li>If we're ready to fire and the target is in the optimal range, stop. (And fire.)</li>
<li>If the target is greater than our optimal range, speed up.</li>
<li>If the target is less than our optimal range, reverse.</li>
</ol>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjunvPg2lFKYXMo4OnF4TQw83EooxjcT1uuQHVewhDMoAcfDjJqDbw1AtZrb9QIV2PMjGJV5_uS9_6GtuY9p2ngjWCftmpl28mZqtl9PnebGK1bVEFplQY2WMASkpAfgOeKBfe98t-8gX9q/s1600/EnemyDetection3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="220" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjunvPg2lFKYXMo4OnF4TQw83EooxjcT1uuQHVewhDMoAcfDjJqDbw1AtZrb9QIV2PMjGJV5_uS9_6GtuY9p2ngjWCftmpl28mZqtl9PnebGK1bVEFplQY2WMASkpAfgOeKBfe98t-8gX9q/s320/EnemyDetection3.png" width="320" /></a></div>
<div style="text-align: center;">
<i>All this code to set Steering and Throttle...</i></div>
<div>
<br /></div>
<div>
Note that this is not the <a href="http://gamedevelopment.tutsplus.com/tutorials/understanding-steering-behaviors-pursuit-and-evade--gamedev-2946">Pursuit behavior</a> in Fernando Bevilacqua's excellent blog. This is simply lining up the shot. We'll build Pursuit behavior later when implementing explicit "attack this target" controls.</div>
<div>
<br /></div>
<div>
<b>Finding LineOfSite Location</b></div>
<div>
<br /></div>
<div>
What if the enemy is in range, but we can't see it due to an obstacle? (A defeated enemy or an ally mentioned earlier.)</div>
<div>
<br /></div>
<div>
We'll implement another Environment Query to find a location to fire from. In this case, we need to create a custom Context because the perspective will be from the <i>target</i>, not our vehicle. So the context needs to peek at the querier's properties to see who it's target is.</div>
<div>
<br /></div>
<div>
Since we have an optimal range we want to fire from, we'll use the built-in Donut generator to create a list of locations around our target within that range. Then test to see which ones the target can see (and our vehicle could see from) and the closest of those to our vehicle:</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjFA60qFjEN2lKmP17kPdjDhUWwHPPIqUIoPzzFIjP7A5Gtjq-2inWs3JhfNJ5COuurntVY79HH8yfL_56TGGsgfv0ezNhSxsMDP7MuxNsY1kaAKE0vHDtghXFrDGPrbJjAz9fTD6tjGOAu/s1600/EnemyDetection5.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="225" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjFA60qFjEN2lKmP17kPdjDhUWwHPPIqUIoPzzFIjP7A5Gtjq-2inWs3JhfNJ5COuurntVY79HH8yfL_56TGGsgfv0ezNhSxsMDP7MuxNsY1kaAKE0vHDtghXFrDGPrbJjAz9fTD6tjGOAu/s320/EnemyDetection5.png" width="320" /></a></div>
<div>
<br /></div>
<div>
That yields:</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRJT1kfOKIcijmbdR0Cs1oL4wR-QdGkhPF8ProbRA0vv3xMYGeoF3dAQhzWLkC1KQcxiR34K0qxOobEoA_0Gu9EypKV71nYl5vr2xe6OTTVKAgJsh7DdzQZ_2LwLQcsRK7L-2MjjTPgWKP/s1600/EnemyDetection4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="197" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRJT1kfOKIcijmbdR0Cs1oL4wR-QdGkhPF8ProbRA0vv3xMYGeoF3dAQhzWLkC1KQcxiR34K0qxOobEoA_0Gu9EypKV71nYl5vr2xe6OTTVKAgJsh7DdzQZ_2LwLQcsRK7L-2MjjTPgWKP/s320/EnemyDetection4.png" width="320" /></a></div>
<div>
<br /></div>
<div>
<b>Challenges</b></div>
<div>
<br /></div>
<div>
I ran into two trouble spots with the EQS.</div>
<div>
<br /></div>
<div>
First, an Environment Query that has no result does not clear its Blackboard entry and set it to None. It simply fails and moves on. That inadvertently leaves the last detected enemy even when none are found. So it was necessary to create a "Clear Found Enemy" task at the start of the Enemy Detection Sequence.</div>
<div>
<br /></div>
<div>
Second, the FindLineOfFire query would not return results when run from a Blueprint. It worked just find from the Behavior Tree or the EQS Test Pawn. It really belongs inside the vehicle's Blueprint, as the Behavior Tree should simply be saying "Attack <i>This</i>" then let it do it's thing. So until that's resolved, it's temporarily in the Behavior Tree.</div>
<div>
<br /></div>
<div>
The Tree looks like this:</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjJyRmGiJDHtcjV0ceVhhOQpAzcq7pukiDFE5x6G7gQNiFNeBesHuMq-UiSRHfUrSE4LCnhaVwc658XSrAEYW0JZ_z3S1-lNkZY-wbNvYV1ZK1gkGFhmj5lNTBQy45JRJ0puCrlHPPE5iqP/s1600/EnemyDetection6.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="136" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjJyRmGiJDHtcjV0ceVhhOQpAzcq7pukiDFE5x6G7gQNiFNeBesHuMq-UiSRHfUrSE4LCnhaVwc658XSrAEYW0JZ_z3S1-lNkZY-wbNvYV1ZK1gkGFhmj5lNTBQy45JRJ0puCrlHPPE5iqP/s320/EnemyDetection6.png" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<i>Ideally, each sequence would only have two tasks...</i></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
Here's what it looks like in action:</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div style="text-align: center;">
<iframe width="320" height="266" class="YOUTUBE-iframe-video" data-thumbnail-src="https://i.ytimg.com/vi/Y8M2HaO-uxo/0.jpg" src="https://www.youtube.com/embed/Y8M2HaO-uxo?feature=player_embedded" frameborder="0" allowfullscreen></iframe></div>
<br />
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
And yes it's finally time to apply some post-apocalyptic skins to the vehicles and attach some very loud guns...</div>
<div>
<br /></div>
ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com0tag:blogger.com,1999:blog-2513175893262458465.post-43559371618118915762015-11-12T18:02:00.000-08:002015-11-12T18:06:34.993-08:00Collision Avoidance for Vehicles in Unreal Engine 4Now that we can successfully <a href="http://zombpir8ninja.blogspot.com/2015/10/box-selecting-multiple-vehicles-in.html">select a group of vehicles and send them to a destination</a> together, we've exposed another issue: they are blissfully unaware of each other and collide like a demolition derby. Not a bad tactic against an enemy, but not helpful alongside your allies.<br />
<br />
<b>Collision Avoidance</b><br />
<br />
Fernando Bevilacqua has a fantastic series on <a href="http://gamedevelopment.tutsplus.com/series/understanding-steering-behaviors--gamedev-12732">Understanding Steering Behaviors</a>. While his code generates forces on sprites and doesn't directly apply to our throttle/steering controls, the theory is still applicable.<br />
<br />
One of the beautiful features of Fernando's approach is that all of his steering behaviors are cumulative and combine to create fluid motion. We'll do the same by injecting a collision avoidance check after the vehicle calculates its navigation controls. This means it will avoid collisions when necessary but will resume traveling toward its destination otherwise.<br />
<br />
Based on Fernando's <a href="http://gamedevelopment.tutsplus.com/tutorials/understanding-steering-behaviors-collision-avoidance--gamedev-7777">Collision Avoidance</a> tutorial, we need to keep the following in mind:<br />
<ul>
<li>Analyze the most threatening obstacle</li>
<li>Longer look-ahead length causes earlier reactions</li>
<li>Detect a potential collision</li>
<li>Apply an avoidance force</li>
</ul>
<div>
Fortunately, Unreal makes the first three easy to implement with <a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/Collision/LineTraceByChannel/index.html">LineTraceByChannel()</a>.</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/Collision/LineTraceByChannel/index.html"><img border="0" height="289" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhmv3xNkzTtr7oR78iUYiWNYIC9NLYnpJ3YQmUXqCL_TpgZyzWdFUwr83887760uthnEoaEtzC9CAwZU0RT77zyGBkzQQSeMVnLS6w4BQHlv4iZVE7uxXqXzKl7_J37EGlKWE6OJ5gJXu9K/s320/LineTraceByChannel.png" width="320" /></a></div>
<div style="text-align: center;">
<i>The scissors in Unreal's <a href="https://en.wikipedia.org/wiki/Swiss_Army_knife">Swiss Army Knife</a>.</i></div>
<div>
<br /></div>
<div>
Setting the End vector to some distance ahead will return a boolean if there's an obstacle. I once again made use of <a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/Game/Components/WheeledVehicleMovement/GetForwardSpeed/index.html">GetForwardSpeed()</a> as the look-ahead length for simplicity. The faster the vehicle is traveling, the farther ahead it needs to check for obstacles to react to.</div>
<div>
<br /></div>
<div>
After experimentation, I projected two collision "sensors" on either side of the vehicle. When only one is used, slight angle differences between nearby vehicles may not trace a hit yet still collide:</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikKlWWHxo0DSF2vn9h5Enj_MAU0PSdHUCMr_BE1sYUy4ZKt8iwSYJa9gxgjhJWKt8eQ9xWs52YnS5B3IRdMB27v7nXGu5GDUTAK8ULjTrSh0YvUmPPRj_32FmMsYAjZZsa3WjQIqIq3F0_/s1600/CollisionAvoidance1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="195" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikKlWWHxo0DSF2vn9h5Enj_MAU0PSdHUCMr_BE1sYUy4ZKt8iwSYJa9gxgjhJWKt8eQ9xWs52YnS5B3IRdMB27v7nXGu5GDUTAK8ULjTrSh0YvUmPPRj_32FmMsYAjZZsa3WjQIqIq3F0_/s320/CollisionAvoidance1.png" width="320" /></a></div>
<div style="text-align: center;">
<i>Looks almost like tracer fire from side-mounted weapons...</i></div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
As for "applying an avoidance force", our limited control options of throttle or steering become a blessing in disguise as we're reduced to two options to react: slow down or turn away.</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
Slowing down was simple to implement: set the throttle to -1. As soon as the obstacle is out of range, pathfinding resumes and they speed up again on their own. While this did reduce the number of collisions, it also caused the vehicles to spread out into a long line.</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
Steering away was only slightly more complicated. In this case, both sensors were necessary to decide which way to turn: away from the side closest to the detected collision. Also an improved reduction of collisions, but they could be going too fast to avoid a collision despite turning.</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
So implementing both became necessary and avoided most collisions while keeping them relatively close together:</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
<div class="separator" style="clear: both; text-align: center;">
<iframe allowfullscreen="" class="YOUTUBE-iframe-video" data-thumbnail-src="https://i.ytimg.com/vi/-0EnkadM-lo/0.jpg" frameborder="0" height="266" src="https://www.youtube.com/embed/-0EnkadM-lo?feature=player_embedded" width="320"></iframe></div>
<div style="text-align: center;">
<i>The collision counter at the top of the preview is with avoidance disabled...</i></div>
</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
<b>Getting Unstuck</b></div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
While we're injecting safety checks, there's another that's desperately needed: getting unstuck when rammed against a wall or another vehicle.</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
This was simply a series of branches, basically the same thing you would do actually driving:</div>
<div style="text-align: left;">
<ul>
<li>If I'm not stuck, constantly reset a StuckTimer</li>
<li>If my throttle is not 0 but my velocity (nearly) is 0, then I could be stuck; watch the StuckTimer</li>
<li>If I'm still not moving after 1 second, set Throttle and Steering opposite to whatever I was doing</li>
<li>After 1 second of doing the opposite, I'm not stuck</li>
</ul>
</div>
<div style="text-align: left;">
Our AI routine still looks manageable:</div>
<div style="text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjq8VLXLVZ8Yb_pJiwmb0RC6EwtpltAnpPma0AEJSmp5465O-YOtTs08MJ05fz6qnVTRHEKVo3zqxpSX_BMorsUR7TIlykgR-vOb2tdLq0BqrRxQfn4h_dsChBcxr0PlfIFURRWBft692cn/s1600/CollisionAvoidance2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="235" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjq8VLXLVZ8Yb_pJiwmb0RC6EwtpltAnpPma0AEJSmp5465O-YOtTs08MJ05fz6qnVTRHEKVo3zqxpSX_BMorsUR7TIlykgR-vOb2tdLq0BqrRxQfn4h_dsChBcxr0PlfIFURRWBft692cn/s320/CollisionAvoidance2.png" width="320" /></a></div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
Our vehicles are fairly autonomous, so the last thing to implement before "Guns!" is enemy detection.</div>
ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com0tag:blogger.com,1999:blog-2513175893262458465.post-27588371380703882942015-10-16T11:24:00.000-07:002015-10-16T11:24:52.180-07:00Box Selecting Multiple Vehicles In Unreal Engine 4We now have our top-down vehicle <a href="http://zombpir8ninja.blogspot.com/2015/09/basic-moveto-behavior-for-vehicles-in.html">moving to a point-and-click destination</a>, <a href="http://zombpir8ninja.blogspot.com/2015/09/improved-moveto-behavior-for-vehicles.html">pathfinding, cornering</a> and <a href="http://zombpir8ninja.blogspot.com/2015/10/parking-in-moveto-behavior-for-vehicles.html">parking</a>. But one vehicle does not constitute a squad.<br />
<br />
I created a hierarchy of structs to setup team compositions:<br />
<ul>
<li>Match</li>
<ul>
<li>Team[]</li>
<ul>
<li>Player</li>
<li>Units[]</li>
<ul>
<li>PawnClass</li>
<li>Active</li>
<li>Debug</li>
<li>Health</li>
<li>etc...</li>
</ul>
</ul>
</ul>
</ul>
<div>
That struct is passed to a UnitManager which handles assigning the teams start locations and spawning units.</div>
<div>
<br /></div>
<div>
So far, fairly straight-forward. To box select units with the mouse:</div>
<div>
<ol>
<li>Get the mouse X,Y position in the HUD with <a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/Game/Player/GetMousePosition/index.html">GetMousePosition</a></li>
<li>As the mouse is moved, pass the start and end positions to the HUD</li>
<li>On the HUD's <a href="https://docs.unrealengine.com/latest/INT/Engine/Blueprints/UserGuide/Events/index.html#eventreceivedrawhud">ReceiveDrawHUD</a> event, <a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/HUD/DrawLine/index.html">DrawLines</a> representing the selection square<br /><br />Meanwhile...<br /> </li>
<li><a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/Game/Player/GetHitResultUnderCursorbyChannel/index.html">GetHitResultUnderCursorByChannel</a> > <a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/Collision/BreakHitResult/index.html">BreakHitResult</a>.HitActor cast to a vehicle unit gets a vehicle clicked on. If the cast fails...</li>
<li>GetHitResultUnderCursorByChannel > BreakHitResult.Location gets the mouse world location</li>
<li>As the mouse is moved, pass the start and end locations to...</li>
</ol>
<div>
Here's where it gets tricky. Initial instinct is to pass it to <a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/Collision/BoxOverlapActors/index.html">BoxOverlapActors</a>. That yields:</div>
</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEizB_AsXRplPTBZ2JC8fQeJBhCdfjaiV-FgyBgbcT-MxcN6_wd3_K1zMGjBlodk1VAAuKKINXZMgM5iWEYzq-L_JeCui5yCTbtBYGJocWokTTF9UgIN6hCbdhcIiXN5V7GERxp6RpSU1jJC/s1600/BoxSelection.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="196" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEizB_AsXRplPTBZ2JC8fQeJBhCdfjaiV-FgyBgbcT-MxcN6_wd3_K1zMGjBlodk1VAAuKKINXZMgM5iWEYzq-L_JeCui5yCTbtBYGJocWokTTF9UgIN6hCbdhcIiXN5V7GERxp6RpSU1jJC/s320/BoxSelection.png" width="320" /></a></div>
<div style="text-align: center;">
<a href="http://muppet.wikia.com/wiki/One_of_These_Things"><i>One of these things is not like the others...</i></a></div>
<div>
<br /></div>
<div>
The problem is caused by the isometric camera perspective. A square drawn on the UI is actually trapezoidal in world space. You can't rely on just two points. You must pass all four UI positions to <a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/Collision/LineTraceByChannel/index.html">LineTraceByChannel</a> to get the world position of each corner, which then represent the four planes of the trapezoid:</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgnsIcndtCd-PmqfgIBRWuMokI6XuwlFgYelYE_8cz_6iQEsa0nRAoYhIM1-RP9E-R6SGly7rcYPZ11OLqoW2N28JLgN_WpavORgBRWqWm26Is24v9yvNJVxfdG5DxGhzCUJ7OzxN3lpaDA/s1600/BoxSelection2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="196" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgnsIcndtCd-PmqfgIBRWuMokI6XuwlFgYelYE_8cz_6iQEsa0nRAoYhIM1-RP9E-R6SGly7rcYPZ11OLqoW2N28JLgN_WpavORgBRWqWm26Is24v9yvNJVxfdG5DxGhzCUJ7OzxN3lpaDA/s320/BoxSelection2.png" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<i>It's a box! <a href="http://www.imdb.com/character/ch0007894/quotes">Just kidding. But not really</a>.</i></div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
So how do you find the vehicles inside a polygon without a "PolygonOverlapActors"? There's a <a href="https://en.wikipedia.org/wiki/Point_in_polygon">simple trick</a>. Trace a ray outward from each vehicle and count the number of polygons it intersects using <a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/Math/Intersection/LinePlaneIntersection_Origin_Nor-/index.html">LinePlaneIntersection</a>. If it intersects two or none, it's outside. If it only intersects one, it's inside:</div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiEXIlKvVYMdQmI8l9ZHPk9j70eF-y9x72yYKukO0UP1Zf-tLH2MSxRhH0NWHL8x97k0fHqj5y-i_dswzmMZeb4QeXCTVhxlP9F8vvN8YHE0PH5gw_r46gRqfkt7qUBxatNeWR9bfSlv49W/s1600/BoxSelection3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="196" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiEXIlKvVYMdQmI8l9ZHPk9j70eF-y9x72yYKukO0UP1Zf-tLH2MSxRhH0NWHL8x97k0fHqj5y-i_dswzmMZeb4QeXCTVhxlP9F8vvN8YHE0PH5gw_r46gRqfkt7qUBxatNeWR9bfSlv49W/s320/BoxSelection3.png" width="320" /></a></div>
<div>
<div style="text-align: center;">
<i>I just added lasers to the vehicle weapon list.</i></div>
</div>
<div>
<br /></div>
We can now control a squad:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<iframe width="320" height="266" class="YOUTUBE-iframe-video" data-thumbnail-src="https://i.ytimg.com/vi/ULDrCb9Vftg/0.jpg" src="https://www.youtube.com/embed/ULDrCb9Vftg?feature=player_embedded" frameborder="0" allowfullscreen></iframe></div>
<br />
Next, we'll get them to be aware of each other and try to avoid colliding. (And yes, it's almost time for guns!)ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com0tag:blogger.com,1999:blog-2513175893262458465.post-19669487101115367532015-10-02T12:39:00.000-07:002015-10-02T17:05:23.662-07:00Parking Using a MoveTo Behavior For Vehicles In Unreal Engine 4Buckle up, this one's going to involve some math. To keep motivated, let's remind ourselves what the goal is:<br />
<br />
<div style="text-align: center;">
<span style="font-size: xx-small;">Image from <a href="http://www.imdb.com/title/tt1392190/mediaindex?page=1&ref_=ttmi_mi_sm">imdb.com</a></span></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGmUqIZmhqjqi_haJGzQp6D5NTAcWyonnd12NmzxGIqew5PErKRVwsSED0x3JgMjkLBjdAOXFOjRLJDwxKOrYv4thkQt7SmKwL51HbIchI8ovgYKipwdK-ezzW3AQ7d09HotTZD0qkPvJq/s1600/MadMaxFuryRoad.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="213" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGmUqIZmhqjqi_haJGzQp6D5NTAcWyonnd12NmzxGIqew5PErKRVwsSED0x3JgMjkLBjdAOXFOjRLJDwxKOrYv4thkQt7SmKwL51HbIchI8ovgYKipwdK-ezzW3AQ7d09HotTZD0qkPvJq/s320/MadMaxFuryRoad.jpg" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<i>This is the view from the NPC. Those would all be mine.</i></div>
<br />
Parking in a specified direction involves two steps: the approach and the landing.<br />
<br />
<b>Parking, the Approach</b><br />
<br />
We could just fling our vehicle directly at a destination and let it line itself up when it gets there. However since there are 360 angles it could arrive at and 360 angles it could be destined for, it may <a href="https://www.youtube.com/watch?v=IGiQOCX9UbM">spend quite some time turning around</a>. So ideally we want it as close as possible to its final rotation when it begins the landing.<br />
<br />
To do that, we're going to inject a waypoint into the array of Path Points returned by the <a href="https://docs.unrealengine.com/latest/INT/API/Runtime/Engine/AI/Navigation/FNavigationPath/index.html">NavigationPath</a> object. We want to calculate a waypoint that is 90 to 180 degrees behind the destination. If the vehicle is in front of the destination, it'll aim for 90 degrees to the side. As the vehicle drives behind the destination, it will curve inward to 180 degrees directly behind the destination:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiP07L97_BJpeyks6t59pbI6xgCsW8taTGFg5rsoU0kMnn98haJ9HU0MjVsGrBjXFP_qFcZRbHHlnq4-lDtKURy0DG5i5CeiVEpcJuz9cYuidDoXUyoV8ZCaJBQ8bf0A24Ev9byw9gYTmRy/s1600/ParkingApproach.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="196" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiP07L97_BJpeyks6t59pbI6xgCsW8taTGFg5rsoU0kMnn98haJ9HU0MjVsGrBjXFP_qFcZRbHHlnq4-lDtKURy0DG5i5CeiVEpcJuz9cYuidDoXUyoV8ZCaJBQ8bf0A24Ev9byw9gYTmRy/s320/ParkingApproach.png" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<i>The similarity to aircraft landing is not lost on me.</i></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
The distance behind the destination (the yellow dotted line above) is determined by:<br />
<br />
<div style="text-align: center;">
<span style="font-family: Courier New, Courier, monospace;">maxDistance * SIN( ABS( DeltaAngleFromNextNavToFinalRotation ) * 0.5 )</span></div>
<div style="text-align: center;">
<br /></div>
<div style="text-align: left;">
I chose 1500 for maxDistance, as longer distances makes the vehicle appear to veer way off course and shorter distances didn't give it enough length to line up behind it.</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
The direction from the destination to insert the waypoint (the orange dotted line above) is:</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: center;">
<span style="font-family: Courier New, Courier, monospace;">COS( DeltaAngleFromNextNavToFinalRotation * 0.333 ) * SIGN( DeltaAngleFromNextNavToFinalRotation ) * -180</span></div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
The result is a rotator from 90 to 180 degrees, to be added to the final rotation. The numbers work out like this:</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: center;">
<table style="border: 1px solid #333; margin-left: 150px;">
<tbody>
<tr><td style="width: 100px;"><b>Angle</b></td><td style="width: 100px;"><b>Rotator</b></td><td style="width: 100px;"><b>Length</b></td>
</tr>
<tr><td>-180</td><td>90.1</td><td>1</td></tr>
<tr><td>-150</td><td>115.8</td><td>0.96</td></tr>
<tr><td>-120</td><td>137.9</td><td>0.86</td></tr>
<tr><td>-90</td><td>155.9</td><td>0.70</td></tr>
<tr><td>-60</td><td>169.1 </td><td>0.5</td></tr>
<tr><td>-30</td><td>177.2 </td><td>0.25</td></tr>
<tr><td>0</td><td>0 </td><td>0</td></tr>
<tr><td>30</td><td>-177.2</td><td>0.25</td></tr>
<tr><td>60</td><td>-169.1 </td><td>0.5</td></tr>
<tr><td>90</td><td>-155.9 </td><td>0.70</td></tr>
<tr><td>120</td><td>-137.9</td><td>0.86</td></tr>
<tr><td>150</td><td>-115.8</td><td>0.96</td></tr>
<tr><td>180</td><td>-90.1</td><td>1</td></tr>
</tbody></table>
</div>
<div style="text-align: left;">
<br /></div>
Here's the blueprint:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiEkqA3BF3Y7z0UApGmkcjsIr9PG0CzB3etKYGPGvYWZJSpdud4AebU-TPs40xiabKk5QoMrvqImxqL9xkvN354C2tmeR-INTvyGBWt-9BvVWLIAr_ezzHrBCUiDLQ6Io4PkaPyZUfH70JQ/s1600/ParkingApproachBlueprint.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="85" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiEkqA3BF3Y7z0UApGmkcjsIr9PG0CzB3etKYGPGvYWZJSpdud4AebU-TPs40xiabKk5QoMrvqImxqL9xkvN354C2tmeR-INTvyGBWt-9BvVWLIAr_ezzHrBCUiDLQ6Io4PkaPyZUfH70JQ/s320/ParkingApproachBlueprint.png" width="320" /></a></div>
<br />
<b>Parking, the Landing</b><br />
<br />
Once the vehicle is likely behind the destination, probably facing nearly the correct direction, and within CloseEnough(), we can start actually parking.<br />
<br />
The primary goal is to align the vehicle along the rotation plane that passes through the destination. That's the hard part. Then it's a simple matter to forward or reverse to the destination.<br />
<br />
We need to calculate a target to steer toward again, but unlike the Approach target, the Landing target will always be in a line along the rotation plane. The farther to the side the vehicle is, the straighter it will aim directly for the plane. As it gets closer, it will steer to drive alongside it:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhcSd_DlLC3cjp2Z8TvP0xYXyI3VZfrcTGFAWQe4YmumDMyC3-ebZfH6jHxxzy3KlOvkA4scOaiyq0tlrLzSWHEL1QBE0TQvpd171qCHCGxT_oegyol6FrpGbbgBH6Nhe9f-sdPOjUMBPXj/s1600/ParkingPlane.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="196" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhcSd_DlLC3cjp2Z8TvP0xYXyI3VZfrcTGFAWQe4YmumDMyC3-ebZfH6jHxxzy3KlOvkA4scOaiyq0tlrLzSWHEL1QBE0TQvpd171qCHCGxT_oegyol6FrpGbbgBH6Nhe9f-sdPOjUMBPXj/s320/ParkingPlane.png" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<i>I would have been more interested in <a href="http://www.mathsisfun.com/sine-cosine-tangent.html">trigonometry</a> if the problems were like this instead of the height of trees.</i></div>
<br />
We need three values: how far the vehicle is along the plane, its distance away from the plane and whether the vehicle is facing toward or away from it.<br />
<br />
We know how far the vehicle is along the plane (the yellow line above) using:<br />
<br />
<div style="text-align: center;">
<span style="font-family: Courier New, Courier, monospace;">COS( DeltaAngleFromFinalRotationToVehicle ) * DistanceFromDestinationToVehicle</span></div>
<br />
And its distance away from the plane (the orange dotted line above) with:<br />
<br />
<div style="text-align: center;">
<span style="font-family: Courier New, Courier, monospace;">SIN( DeltaAngleFromFinalRotationToVehicle ) * DistanceFromDestinationToVehicle</span></div>
<br />
We can tell if the vehicle is aimed toward or away from the plane by:<br />
<br />
<div style="text-align: center;">
<span style="font-family: Courier New, Courier, monospace;">SIGN( DeltaBetweenFinalRotationAndVehiclesRotation ) *</span><br />
<span style="font-family: Courier New, Courier, monospace;">SIGN( DOTPRODUCT( DestinationRightVector, VehicleRightVector ) )</span></div>
<br />
That results in a 1 or -1 describing whether the vehicle is aiming left or right of the destination and which side of the plane it's on. That becomes the vehicle's throttle.<br />
<br />
Finally, we can calculate an exponentially increasing length away from the vehicle's position along the plane:<br />
<br />
<div style="text-align: center;">
<span style="font-family: Courier New, Courier, monospace;">ABS( ( 1 - ( CloseEnough / ABS( DistanceFromPlane ) ) ) * DistanceAlongPlane ) * Throttle + DistanceAlongPlane</span></div>
<br />
The blueprints:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjD1Ivwwza6ARRBragANnnhVXDEMOiw5lSrjAN5rIoHH4iXZumEsCz-Hk6eO0DGvsC7IZbL2fMLpU45bKYXVn3Jqn9W_gXHmpF2yVMgjP9-ypM2W2Zm5nJI534k4HMp4rTPUVQPlNiajAzA/s1600/ParkingLandingBlueprint1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="259" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjD1Ivwwza6ARRBragANnnhVXDEMOiw5lSrjAN5rIoHH4iXZumEsCz-Hk6eO0DGvsC7IZbL2fMLpU45bKYXVn3Jqn9W_gXHmpF2yVMgjP9-ypM2W2Zm5nJI534k4HMp4rTPUVQPlNiajAzA/s320/ParkingLandingBlueprint1.png" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhj1qcvrmet9ASFq87CD7CE5iagYjKbP6w4QHPBPQ2OIvQezMK9cfVYBzC6LZFYu1Qk3TpFWVLaARxkfZocfVyqJZb49VH0T4JSgu_g8cm1zfafJisaFRcVu9u8vnfJs0cjccfoonq_Hb0Y/s1600/ParkingLandingBlueprint2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="71" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhj1qcvrmet9ASFq87CD7CE5iagYjKbP6w4QHPBPQ2OIvQezMK9cfVYBzC6LZFYu1Qk3TpFWVLaARxkfZocfVyqJZb49VH0T4JSgu_g8cm1zfafJisaFRcVu9u8vnfJs0cjccfoonq_Hb0Y/s320/ParkingLandingBlueprint2.png" width="320" /></a></div>
<div style="text-align: center;">
<i>You said THREE values?!</i></div>
<br />
Will it fit <a href="https://www.youtube.com/watch?v=3nOxdKcqC_I">like a glove</a>? Probably not. Could wasteland warriors use it to setup an ambush? Absolutely:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<iframe allowfullscreen="" class="YOUTUBE-iframe-video" data-thumbnail-src="https://i.ytimg.com/vi/h5p6e9kjvww/0.jpg" frameborder="0" height="266" src="https://www.youtube.com/embed/h5p6e9kjvww?feature=player_embedded" width="320"></iframe></div>
<br />
<br />
The logic still isn't too complicated yet:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtRWRgF5yEm45lvHO6gz7rVIb4zScdyzJz6oRJ7ihmCRJ0lQrDHwu4IGF3jQdVXTz9Kj_qtvj50Fy9lfBW_xcpbRMgyJuZlNUILy0O7mOSAWp9QJF7i7XsOJLLRxYwlYBOc1A_iludpU09/s1600/ParkingVehicleMoveTo.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtRWRgF5yEm45lvHO6gz7rVIb4zScdyzJz6oRJ7ihmCRJ0lQrDHwu4IGF3jQdVXTz9Kj_qtvj50Fy9lfBW_xcpbRMgyJuZlNUILy0O7mOSAWp9QJF7i7XsOJLLRxYwlYBOc1A_iludpU09/s320/ParkingVehicleMoveTo.png" width="315" /></a></div>
<br />
Next time, we'll spawn multiple vehicles and handle group selection.ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com0tag:blogger.com,1999:blog-2513175893262458465.post-79018841540557619942015-09-18T14:06:00.001-07:002015-09-18T14:08:56.255-07:00Improved MoveTo Behavior For Vehicles In Unreal Engine 4<br />
After the <a href="http://zombpir8ninja.blogspot.com/2015/09/basic-moveto-behavior-for-vehicles-in.html">previous post</a>, we now have rudimentary MoveTo functionality for WheeledVehicle pawns. However, locking the throttle axis at 1.0 causes some obvious problems steering around corners.<br />
<br />
Before we tackle that, let's at least stop the vehicle when it reaches its destination.<br />
<br />
<b>Close Enough</b><br />
<br />
I added a CloseEnough float variable to the unit, then compared it to the distance to the last NavPoint[] inside a CalculateThrottle() function. If it's close enough, we'll set the vehicle's state to "Parking".<br />
<br />
Yes, state machines are <a href="http://aigamedev.com/open/article/fsm-age-is-over/">out of fashion</a> and behavior trees have <a href="http://guineashots.com/2014/07/25/an-introduction-to-behavior-trees-part-1/">obvious advantages</a>. At first, I did build quite a bit of control logic inside the behavior tree. Yet as I continuously added variables to the <a href="https://forums.unrealengine.com/showthread.php?2004-Blackboard-Documentation">Blackboard</a> I realized I would then need multiple behavior trees: one for each type of unit. A helicopter would need vastly different controls. I would have duplicate trees (or extensive <a href="https://docs.unrealengine.com/latest/INT/Engine/Blueprints/UserGuide/FlowControl/index.html#editingswitchnodes">Switches</a>) despite having very similar behaviors.<br />
<br />
I think there's still room for both, and a mix of the two is needed here. The Behavior tree will generally define what the unit <i>should</i> be doing (navigating, following, attacking...) while the unit itself implements <i>how </i>to do it -- including how to stop doing it.<br />
<br />
So in this case, the Behavior Tree asks "Am I Driving?" (Driving is the state set when MoveTo is called.) If so, find a path. The vehicle itself determines how it gets there. When the answer is no, the Behavior Tree will "idle" until it's given a new direction. When Parking, the vehicle sets the throttle to 0 and enables the handbrake.<br />
<br />
<b>Improved Throttling</b><br />
<br />
At first, I approached cornering as a math problem and tried <a href="http://cdn.intechopen.com/pdfs-wm/39283.pdf">physics calculations</a> to determine control settings around a corner. After <a href="http://edison.rutgers.edu/newsletter9.html#4">numerous failures</a>, it struck me that I don't calculate any of that in my head when I drive my own car.<br />
<br />
So I enabled possession of a unit and had it drop pylons every time I changed a control.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhnWhI-m51D7ym3mR6REHNGyrP2-Mws-cdNQomMkUwf8P_tio8k6un1FQBMFniOplhKkZy7KOXjPRhLdfhatNbB32OKTkCZ_VMbmZsedsYjAM1XMfFTpz7q779ideQjnsiMLw4udqZ2ai6x/s1600/RecordingManualControls.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="195" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhnWhI-m51D7ym3mR6REHNGyrP2-Mws-cdNQomMkUwf8P_tio8k6un1FQBMFniOplhKkZy7KOXjPRhLdfhatNbB32OKTkCZ_VMbmZsedsYjAM1XMfFTpz7q779ideQjnsiMLw4udqZ2ai6x/s320/RecordingManualControls.png" width="320" /></a></div>
<div style="text-align: center;">
Also recording steering: Not as useful</div>
<br />
While I always steer toward my next immediate goal, throttle is determined by a future guess as to where the vehicle be and what direction it will be facing. Despite steering straight for the corner, I stop accelerating to prepare for the turn. Once in the turn, if I'm going too fast and not facing my next goal then I apply the handbrake and throttle to force a tighter turn.<br />
<br />
I chose to predict the position of the car after one second, purely because it was easy to use <a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/Game/Components/WheeledVehicleMovement/GetForwardSpeed/index.html">GetForwardSpeed()</a> and apply that to the forward vector.<br />
<br />
Armed with those few rules, the logic is actually quite simple:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiDM4moT4nt8VpymmJNNxlOJ9TD7qjsgrzz4wSfly2u2a9O76gzIe30Lr3iK9z-v3qw1uEX-7G8UqaP5vXEIA2qTgh4ROykfdoC1HJsUsGrMrHfKkUhSpXyKim3KFvqbzAWxHu_vqfEu1WH/s1600/ImprovedThrottle1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="195" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiDM4moT4nt8VpymmJNNxlOJ9TD7qjsgrzz4wSfly2u2a9O76gzIe30Lr3iK9z-v3qw1uEX-7G8UqaP5vXEIA2qTgh4ROykfdoC1HJsUsGrMrHfKkUhSpXyKim3KFvqbzAWxHu_vqfEu1WH/s320/ImprovedThrottle1.png" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
If the next NavPoint past the predicted distance is NavPoint[1], throttle = 1</div>
<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi6Cv5LIPHZeDrIeZMtv09_0vOSCLf48wwxPxpoMzOgJq2Cjz0JEY5erAW13RzWPkovCIGDH_dlWkVhp7pWt4MDlgn7IvbrCOiJE1TgNETHJ8MBEaudD3BaUMhUhyvyIq1MJvRQinU0td2K/s1600/ImprovedThrottle2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="195" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi6Cv5LIPHZeDrIeZMtv09_0vOSCLf48wwxPxpoMzOgJq2Cjz0JEY5erAW13RzWPkovCIGDH_dlWkVhp7pWt4MDlgn7IvbrCOiJE1TgNETHJ8MBEaudD3BaUMhUhyvyIq1MJvRQinU0td2K/s320/ImprovedThrottle2.png" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
If steering is straight but the angle to the predicted NavPoint is increasing, throttle = 0</div>
<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi29NVfB__kkMYMEJrlS76AiCfBDA9kmg5eR-Bgr-zgN7dtMTvORpdfnZ3m2t-eRO-j4qOsEgY0nrws6Otc68ZwdsfGWMkSdImggqgbqbzLuu-AGCp71kGyYXv313FZoh2lMIoDmwLRnk-p/s1600/ImprovedThrottle3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="195" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi29NVfB__kkMYMEJrlS76AiCfBDA9kmg5eR-Bgr-zgN7dtMTvORpdfnZ3m2t-eRO-j4qOsEgY0nrws6Otc68ZwdsfGWMkSdImggqgbqbzLuu-AGCp71kGyYXv313FZoh2lMIoDmwLRnk-p/s320/ImprovedThrottle3.png" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
If steering is not straight and angle is greater than 90, apply handbrake and throttle = 1</div>
<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJLN_d7to4T-GF_FUx-7mPa7gU-ik8V7_xwrBE8fTaSCtTA7y9xg1roLcubLz1YUoUP_Zi69WukBEH7b2QZlxF0IxMULCwr63KCtyF-wBhSePV2ahb39rgaNj8P_m_vVOVbgD_NBUh77AP/s1600/ImprovedThrottle4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="195" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJLN_d7to4T-GF_FUx-7mPa7gU-ik8V7_xwrBE8fTaSCtTA7y9xg1roLcubLz1YUoUP_Zi69WukBEH7b2QZlxF0IxMULCwr63KCtyF-wBhSePV2ahb39rgaNj8P_m_vVOVbgD_NBUh77AP/s320/ImprovedThrottle4.png" width="320" /></a></div>
<div style="text-align: center;">
Navigation has been recalculated, it has a straight line to the first point NavPoint[1],<br />
throttle = 1</div>
<br />
Will it beat the player in a speed race? Probably not. Is it sufficient to simulate fearless post-apocalyptic survivors? Surprisingly so:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<iframe allowfullscreen="" class="YOUTUBE-iframe-video" data-thumbnail-src="https://i.ytimg.com/vi/c1ozVjaZsEg/0.jpg" frameborder="0" height="266" src="https://www.youtube.com/embed/c1ozVjaZsEg?feature=player_embedded" width="320"></iframe></div>
<br />
<br />
The logic now looks like:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtkNotxDG26__LG8CHGD0JyAFhybp4T8U1E-7KCI7qj_Gxfjokt5ci0_I0H4CwySApZBrsPtCkkit9qjvVtIFiPO4204uCxY9DH_c-wswWTQLe-pgENtqjfRno3cWAOcxJu9AhQCu4DvBv/s1600/ImprovedVehicleMoveTo.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="246" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtkNotxDG26__LG8CHGD0JyAFhybp4T8U1E-7KCI7qj_Gxfjokt5ci0_I0H4CwySApZBrsPtCkkit9qjvVtIFiPO4204uCxY9DH_c-wswWTQLe-pgENtqjfRno3cWAOcxJu9AhQCu4DvBv/s320/ImprovedVehicleMoveTo.png" width="320" /></a></div>
<br />
<br />
<b>Spline Paths</b><br />
<br />
You'll notice that the rendered path (the green lines) expresses curves because I'm using a <a href="https://docs.unrealengine.com/latest/INT/Resources/ContentExamples/Blueprint_Splines/">SplineComponent</a> to connect the NavPoints. Let me save you some trouble lest you attempt to guide the vehicle with a spline or use it to predict the vehicle's position.<br />
<br />
You can't specify the rotation of individual SplineComponent points. If you watch carefully in the videos or screenshots, you'll sometimes see exaggerated curves between some points as their rotations do not necessarily aim toward each other.<br />
<br />
While the SplineComponent provides a very handy <a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/Spline/GetLocationatDistanceAlongSpline/index.html">GetLocationAtDistanceAlongSpline()</a> function, there is a significant amount of bookkeeping required to calculate that effectively as well as the curves mentioned above distorting the results. I found it much simpler to sum up the NavPoint segment lengths and check if my predicted distance is greater.<br />
<br />
Next, we'll improve parking so that the vehicle stops at its destination facing a specified direction.ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com1tag:blogger.com,1999:blog-2513175893262458465.post-85237206542096800212015-09-04T20:12:00.000-07:002015-09-23T08:59:04.767-07:00Basic MoveTo Behavior For Vehicles In Unreal Engine 4<a href="https://en.wikipedia.org/wiki/Baldur%27s_Gate">Baldur's Gate</a>, <a href="https://en.wikipedia.org/wiki/MechCommander">MechCommander</a>, <a href="https://en.wikipedia.org/wiki/Neverwinter_Nights">Neverwinter Nights</a>, <a href="https://en.wikipedia.org/wiki/Dragon_Age:_Origins">Dragon Age: Origins</a> and <a href="https://en.wikipedia.org/wiki/Pillars_of_Eternity">Pillars of Eternity</a>: games featuring squad-level <a href="https://en.wikipedia.org/wiki/Turns,_rounds_and_time-keeping_systems_in_games#Pausable_real-time">real-time with pause</a> tactical gameplay. Those beloved franchises balance a mix of RPG micromanagement and battlefield strategy, but there's a void. <br />
<br />
As a fan of <a href="https://en.wikipedia.org/wiki/Car_Wars">Car Wars</a>, <a href="https://en.wikipedia.org/wiki/Mad_Max_2">The Road Warrior</a>, <a href="https://en.wikipedia.org/wiki/Spy_Hunter">Spy Hunter</a> and <a href="https://en.wikipedia.org/wiki/Death_Race_(film)">Death Race</a>, I want to arm and armor a squad of post-apocalyptic muscle cars then send them into the wasteland to challenge gangs of jury-rigged monstrosities-on-wheels fighting over gasoline and spare parts.<br />
<br />
<div align="center">
<div class="separator" style="clear: both; text-align: center;">
<span style="font-size: xx-small;">Image from </span><a href="http://www.imdb.com/media/rm986034176/tt0452608?ref_=ttmi_mi_all_sf_37" style="font-size: x-small;">imdb.com</a></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEht91s4rmqrrtUzPJO7YlTR1wTkRUJhOhHAREfulV8Kjjca-Hgw2-8EqDFdxOm7V0iLN90-JoKJjIbA3w3yW9hGhWvq-sois1E7UvF211UKaxYGwbY8zXbdvil3BrrX8KkfScbsTdW62PLh/s1600/MV5BMTQxMzk0OTMyN15BMl5BanBnXkFtZTcwNTk1ODgxNw%2540%2540._V1__SX1327_SY834_.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="223" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEht91s4rmqrrtUzPJO7YlTR1wTkRUJhOhHAREfulV8Kjjca-Hgw2-8EqDFdxOm7V0iLN90-JoKJjIbA3w3yW9hGhWvq-sois1E7UvF211UKaxYGwbY8zXbdvil3BrrX8KkfScbsTdW62PLh/s400/MV5BMTQxMzk0OTMyN15BMl5BanBnXkFtZTcwNTk1ODgxNw%2540%2540._V1__SX1327_SY834_.jpg" width="400" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<i>This. A screen full of this.</i></div>
</div>
<br />
Engines like <a href="https://www.unrealengine.com/what-is-unreal-engine-4">Unreal</a> (and <a href="http://unity3d.com/">Unity</a>) are available for free and it's easier than ever for inexperienced developers to create games. With no eagerly anticipated announcements forthcoming, like any smug mature gamer with a programming background I thought, "How hard could it be?"<br />
<br />
As we're going to find out, fairly hard.<br />
<br />
<b>"Simple" Vehicle Movement</b><br />
<br />
The first feature to tackle is point-and-click movement. Instead of directly controlling a single vehicle, we want a squad of vehicles to drive themselves to a specified destination.<br />
<br />
I assumed it would be as simple as importing a vehicle from one of the examples, build a <a href="https://docs.unrealengine.com/latest/INT/Engine/AI/BehaviorTrees/QuickStart/12/index.html">Behavior Tree Task</a> and send it a <a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/AI/AIMoveTo/index.html">MoveTo</a>.<br />
<br />
<div align="center">
<div class="separator" style="clear: both; text-align: center;">
<a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/AI/AIMoveTo/index.html"><img border="0" height="229" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhX_SrtlJYHEpyL_G9Y3Pi3-zeGkDz_ESvFcPcJItVDkBcEoK8VudHZAVzVLHw5D9u_pgKbc06C4sCLTffu7T91CHPxJIpRM93PsLacKUw3mPwbsEpqIk6dfub_Sd-ipRzHtMX7w10EELZ6/s320/AIMoveTo.png" width="320" /></a></div>
<i>Done. Next on the list? Guns!</i></div>
<br />
Except that pawns using the <a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/Game/Components/WheeledVehicleMovement/index.html">WheeledVehicleMovement</a> component don't respond to MoveTo. They respond to Throttle, Steering and Handbrake. So not only do we have to tell it where to go, but how to get there.<br />
<br />
The typical approach to <a href="https://view.officeapps.live.com/op/view.aspx?src=http%3A%2F%2Fwww.cse.lehigh.edu%2F~munoz%2FCSE348%2Fclasses%2FRacingGames2010.pptx">racing game AI</a> is to pre-build waypoints into a circular track. That won't work for this game due to the open-world style maps. We need to tell a vehicle to move from anywhere to anywhere. Fortunately, we can still use Unreal's <a href="https://docs.unrealengine.com/latest/INT/Engine/AI/BehaviorTrees/QuickStart/2/index.html">NavMesh</a> intended for character pawns. The <a href="https://docs.unrealengine.com/latest/INT/BlueprintAPI/AI/Navigation/FindPathtoLocationSynchronously/index.html">FindPathToLocationSynchronously</a> function simply needs Start and End vectors. The result is a <a href="https://docs.unrealengine.com/latest/INT/API/Runtime/Engine/AI/Navigation/FNavigationPath/index.html">NavigationPath</a> object, which includes an array of Path Point vectors.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBSlhWKXPbhvCyjEPULL7f3mjIUigA75Epk2sN-tIsOGeOLt_T0Vz1finyemkXGhB-ZEb3nbLoZkhFnxULTU6MWOD9XDWttuVG52nzzbIMoUxyo8KvLk3JcWFmkf5zHOFY1jc1mJXZLztD/s1600/FindPathToLocation.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="279" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBSlhWKXPbhvCyjEPULL7f3mjIUigA75Epk2sN-tIsOGeOLt_T0Vz1finyemkXGhB-ZEb3nbLoZkhFnxULTU6MWOD9XDWttuVG52nzzbIMoUxyo8KvLk3JcWFmkf5zHOFY1jc1mJXZLztD/s320/FindPathToLocation.png" width="320" /></a></div>
<div style="text-align: center;">
<i>Who says A* Pathfinding is complicated?</i></div>
<br />
Now we can build a short <a href="https://docs.unrealengine.com/latest/INT/Engine/AI/BehaviorTrees/NodeReference/Services/index.html">Behavior Tree Service</a> that recalculates a path from the vehicle's location to its destination. Finally, have the vehicle always steer toward the next waypoint, NavPoints[1], and set the throttle to 1.0 (for now). Our logic so far looks like:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi76URtjOXMU4pD-iZIEKQ8Obf9jycYEbSfDk6GThqY2yn55tkUew38RFqCcFQur4KczJkVA8UqWCK5_cXmAHagDRjiXskGggAqQ0VAzqvt4K4ZsLbHpgpjAXBZtYnAB3X2xzk5-bMP3gKh/s1600/BasicVehicleMoveTo.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi76URtjOXMU4pD-iZIEKQ8Obf9jycYEbSfDk6GThqY2yn55tkUew38RFqCcFQur4KczJkVA8UqWCK5_cXmAHagDRjiXskGggAqQ0VAzqvt4K4ZsLbHpgpjAXBZtYnAB3X2xzk5-bMP3gKh/s320/BasicVehicleMoveTo.png" width="189" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<i>I thought you said this was hard?</i></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
Here's what it looks like in-game:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<iframe allowfullscreen="" class="YOUTUBE-iframe-video" data-thumbnail-src="https://i.ytimg.com/vi/b4go9u6NSPg/0.jpg" frameborder="0" height="266" src="https://www.youtube.com/embed/b4go9u6NSPg?feature=player_embedded" width="320"></iframe></div>
<br />
<br />
Next time, we'll make the throttle smarter and make use of the handbrake.<br />
<br />
<br />ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com12tag:blogger.com,1999:blog-2513175893262458465.post-33743022816815768762011-06-26T06:44:00.000-07:002011-06-26T06:44:21.991-07:00D&D BattleMap Using the UDK – Transitions, Menus and Config FilesThe UI for our placeable object commands isn’t quite ready yet (it seems everyone’s suddenly a critic), but this post we’ll introduce similar concepts while implementing another desperately needed feature: transitioning between maps. I’m getting a little tired of having to close down the UDK and reconfiguring the Unreal FrontEnd just to change maps:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><iframe allowfullscreen='allowfullscreen' webkitallowfullscreen='webkitallowfullscreen' mozallowfullscreen='mozallowfullscreen' width='320' height='266' src='https://www.youtube.com/embed/wGVuB6-YSw8?feature=player_embedded' frameborder='0'></iframe></div><br />
<br />
We’ll provide the option to choose a map with a flyout menu built in Flash. But to achieve our objective of keeping the maps independent of the BattleMap mod, we can’t hard code the map list into the menu. Therefore, we’ll store the list in a config file and pass it to the HUD on startup.<br />
<br />
<strong>Flash</strong><br />
<br />
Once again, our friends at <a href="http://monsterlayer.com/">MonsterLayer.com</a> produced some gorgeous assets for us in an astonishingly short amount of time. I’ll do my best to break down how they built our menu, but you can certainly create any style of menu that suits you. All that’s important is that the asset names match the code.<br />
<br />
Below is a snapshot of an individual menu button. This will be duplicated multiple times: one for each map.<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgqMSr9wPWk0bdaBe2VfSzsE41cbpJE2CvcaS3XL8koG7FYJLeRcPCnkb-hI6gZE-Knq6lMJEn1-tPHX4Q39bPSVQ8PgObySAhArtTmhirEtgudFtsBZJrljRwb38866sbBhLHOPBh8OADm/s1600/Flash1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgqMSr9wPWk0bdaBe2VfSzsE41cbpJE2CvcaS3XL8koG7FYJLeRcPCnkb-hI6gZE-Knq6lMJEn1-tPHX4Q39bPSVQ8PgObySAhArtTmhirEtgudFtsBZJrljRwb38866sbBhLHOPBh8OADm/s320/Flash1.png" width="304" /></a></div><br />
The “Scripts” layer simply contains:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">stop();</span></blockquote>The second frame of the background later changes the color. That will become the “hover” when moused over. (Yes, Flash does include a Button type. However, we had multiple issues trying to get it to work correctly so we went with the programmer’s approach of just coding it ourselves.)<br />
<br />
Make sure the Text object is named “ButtonText” and save the symbol as a MovieClip named “MapButton”.<br />
<br />
Create another button the exact same way:<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg6t8EQN8pIA5jlmtbabyQbCgXSgO-uwvMMM2qrKwt9vXdlpax5Agfe_MKVQM4Fv2hpW2L5CgoT-Q4nU5R36c4WPvqPbXmsa0KbFr2QJ3Noth27MqrsU7BxxcBoA93OoN3VwYj3mtG235Im/s1600/Flash3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg6t8EQN8pIA5jlmtbabyQbCgXSgO-uwvMMM2qrKwt9vXdlpax5Agfe_MKVQM4Fv2hpW2L5CgoT-Q4nU5R36c4WPvqPbXmsa0KbFr2QJ3Noth27MqrsU7BxxcBoA93OoN3VwYj3mtG235Im/s320/Flash3.png" width="186" /></a></div><br />
Name this MovieClip “CloseButton”.<br />
<br />
Next, create a menu that contains your button objects:<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhjGJiHjwuom05hzq3emDPhL4L22wG-4oeg5T-DpNRoRvHdrG8-uAGdvViMZeOYAIr_laH76Aeipsbi5EkLpJNRKSKNhKmmi6PKpALLhPL3TIYJBk1JqnDZIJsZbVdbFFGhhSAV_HhkMSha/s1600/Flash2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="218" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhjGJiHjwuom05hzq3emDPhL4L22wG-4oeg5T-DpNRoRvHdrG8-uAGdvViMZeOYAIr_laH76Aeipsbi5EkLpJNRKSKNhKmmi6PKpALLhPL3TIYJBk1JqnDZIJsZbVdbFFGhhSAV_HhkMSha/s320/Flash2.png" width="320" /></a></div><br />
Name the map button instances “MapButton0”, “MapButton1”, etc. Name the close button instance “CloseButton”. Save this MovieClip as “MapList”.<br />
<br />
Create another MovieClip called “MapListMenu”. Add an instance of MapList named “MapListInst”. Create a motion tween of it moving onto the stage:<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtuBRjoNr1K52i4z63-54AjbMYqY6jd-hvEHcH8U1ubh4gB_pikZCdLiu4cffXGEaq9vUt2-itPav2cn0oLLeWAYRZYBmB-FoLnDZQi0Pht44wCfEqi_mnY4rC4afT8oi8v35x8-IP0AHx/s1600/Flash4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtuBRjoNr1K52i4z63-54AjbMYqY6jd-hvEHcH8U1ubh4gB_pikZCdLiu4cffXGEaq9vUt2-itPav2cn0oLLeWAYRZYBmB-FoLnDZQi0Pht44wCfEqi_mnY4rC4afT8oi8v35x8-IP0AHx/s320/Flash4.png" width="159" /></a></div><br />
The scripts on the 1st and last frames both simply contain:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">stop();</span></blockquote>Now, add an instance of MapListMenu to your root movie named “MapListMenuInst”. Position it so that it starts outside the scene, but ends inside at the end of its animation. (Edit In Place comes in real handy here.)<br />
<br />
We need one more object. Transitioning between maps can take a few seconds to initiate. We should provide a visual clue that the transition has begun and the UDK isn’t hung. Create an object named “Overlay”. I drew a giant semi-transparent background with the ever-polite instructions “Please Wait…”:<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmYEgUquoCkqxKmrGRw0dTqq0NpnrIkt8aag9q4WxxReEUKJQD-ST-Bo4XDeCR7puOmqLSdhROu02dcSFQNTB1K3nOxC5pWw9AwPUWT-H5PH85EjMVaf6dQItWpHNkt-JCCpVwwu1IhrcH/s1600/Flash5.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="226" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmYEgUquoCkqxKmrGRw0dTqq0NpnrIkt8aag9q4WxxReEUKJQD-ST-Bo4XDeCR7puOmqLSdhROu02dcSFQNTB1K3nOxC5pWw9AwPUWT-H5PH85EjMVaf6dQItWpHNkt-JCCpVwwu1IhrcH/s320/Flash5.png" width="320" /></a></div><br />
Add an instance of your Overlay object to your root movie as “OverlayInst”.<br />
<br />
<strong>ActionScript</strong><br />
<br />
Now, we need to wire it all up. Add the following to your root ActionScript. It initializes an array to store the map list, hides the Overlay and sets up mouse events for the buttons. The click event for the map buttons will show the Overlay on top, toggle off the menu and pass the chosen map back to UnrealScript:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">var Maps:Array;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// Hide overlay<br />
OverlayInst._visible = false;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// Init map buttons<br />
i=0;<br />
while(MapListMenuInst.MapListInst["MapButton"+i])<br />
{<br />
MapListMenuInst.MapListInst["MapButton"+i]._visible = false;<br />
MapListMenuInst.MapListInst["MapButton"+i].onRollOver = function(){<br />
this.gotoAndStop(2);<br />
}<br />
MapListMenuInst.MapListInst["MapButton"+i].onRollOut = function(){<br />
this.gotoAndStop(1);<br />
}<br />
MapListMenuInst.MapListInst["MapButton"+i].onRelease = function(){<br />
OverlayInst.swapDepths(_root.getNextHighestDepth());<br />
OverlayInst._visible = true;<br />
ShowMapList();<br />
ExternalInterface.call( "OpenMap", Maps[this._name.substr(9)]["File"] );<br />
}<br />
i++;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// Init close button<br />
MapListMenuInst.MapListInst.CloseButton.onRollOver = function(){<br />
this.gotoAndStop(2);<br />
}<br />
MapListMenuInst.MapListInst.CloseButton.onRollOut = function(){<br />
this.gotoAndStop(1);<br />
}<br />
MapListMenuInst.MapListInst.CloseButton.onRelease = function(){<br />
ExternalInterface.call( "CloseMap" );<br />
}</span></blockquote>Next, add the following InitMapList() function. This will be called by UnrealScript to pass in the map list and setup the map buttons:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> function InitMapList(Param1:Array)<br />
{<br />
Maps = Param1;<br />
<br />
//Show only buttons with maps<br />
for (i=0; i<Maps.length; i++)<br />
{<br />
MapListMenuInst.MapListInst["MapButton"+i].ButtonText.text = Maps[i]["Name"];<br />
MapListMenuInst.MapListInst["MapButton"+i]._visible = true;<br />
}<br />
}</span></blockquote>Finally, add the ShowMapList() function to toggle the menu on/off:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">function ShowMapList()<br />
{<br />
if (MapListMenuInst._currentframe == 1)<br />
{<br />
MapListMenuInst.swapDepths(_root.getNextHighestDepth());<br />
CursorInst.swapDepths(_root.getNextHighestDepth());<br />
MapListMenuInst.gotoAndPlay(2);<br />
}<br />
else<br />
MapListMenuInst.gotoAndStop(1);<br />
}</span></blockquote>Republish the .swf file then open up the UDK, find the BattleMapHud package, and reimport BMHud.<br />
<br />
<strong>BattleMapConfig.uc</strong><br />
<br />
Now, let’s setup a config file to store the map names. The UDK builds in configuration file functionality into the base Object class. That means that every class can implement its own config file. <a href="http://www.moug-portfolio.info/index.php?page=configuration-files">Mougli’s portfolio</a> includes a very good tutorial on UDK configuration files. To sum up, any variable declared globally in a class becomes an entry in its config file.<br />
<br />
Here’s the code for a simple object containing an array of MapItem structs:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">class BattleMapConfig extends Object config(BattleMap);</span><br />
<span style="font-family: "Courier New", Courier, monospace;">struct MapItem<br />
{<br />
var config string Name;<br />
var config string File;<br />
};</span><br />
<span style="font-family: "Courier New", Courier, monospace;">var config array <MapItem> Maps;</span></blockquote>The “config(BattleMap)” directive tells the UDK that this object will read and write to a BattleMap.ini file.<br />
<br />
<strong>DefaultBattleMap.ini</strong><br />
<br />
In your /UDKGame/Config directory, create a new .ini file called “DefaultBattleMap.ini” and enter the titles and file names of your maps. For example, mine looks like:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> [BattleMap.BattleMapConfig]<br />
Maps=(Name="Demon Queen's Enclave U1, L1",File="BM_DemonQueensEnclave.udk")<br />
Maps=(Name="Demon Queen's Enclave L13-16",File="BM_DemonQueensEnclave1.udk")<br />
Maps=(Name="Demon Queen's Enclave L2-6,11,V1",File="BM_DemonQueensEnclave2.udk")<br />
Maps=(Name="Demon Queen's Enclave V2,V4-V10",File="BM_DemonQueensEnclave3.udk")<br />
Maps=(Name="Demon Queen's Enclave V12",File="BM_DemonQueensEnclave4.udk")</span></blockquote>The format should look very familiar, it’s similar to DefaultInput.ini where we include new key bindings. Notice that the section heading is the name of our class.<br />
<br />
<strong>DefaultInput.ini</strong><br />
<br />
Speaking of DefaultInput.ini, while we’re here go ahead and add a key binding to toggle our menu:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> -Bindings=(Name="Escape",Command="GBA_ShowMenu")<br />
.Bindings=(Name="Escape",Command="BMShowMapList")</span></blockquote>(In case you’re curious, the period in front of .Bindings means that duplicate entries are allowed.)<br />
<br />
<strong>BattleMapPlayerController.uc</strong><br />
<br />
Here, we simply need to instantiate our new BattleMapConfig class, which will cause it to automatically initialize its variables from the config file:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">var BattleMapConfig BMConfig;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">simulated function PostBeginPlay()<br />
{<br />
super.PostBeginPlay();<br />
BMConfig = new class'BattleMapConfig';<br />
}</span></blockquote><strong>BattleMapHUD.uc</strong><br />
<br />
Inside PostBeginPlay(), add a call to a CallInitMapList() function right after the CrosshairMovie.Initialize() statement and pass in the newly loaded Map array:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> CrosshairMovie.CallInitMapList(BattleMapPlayerController(PlayerOwner).BMConfig.Maps);</span></blockquote><strong>BattleMapPlayerInput.uc</strong><br />
<br />
Create a new command to tell the HUD to toggle the menu and another to tell the UDK to load a new map:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">exec function BMShowMapList()<br />
{<br />
if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)<br />
{<br />
BattleMapHUD(myHUD).CrossHairMovie.CallShowMapList();<br />
}<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">exec function BMOpenMap(string MapFile)<br />
{<br />
WorldInfo.Game.ProcessServerTravel(MapFile);<br />
}</span><strong></strong></blockquote><strong>BattleMapGfxHud.uc</strong><br />
<br />
First, create two new wrapper functions for calling the HUD’s InitMapList() and ShowMapList():<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">function CallInitMapList( array <MapItem> Param1 )<br />
{<br />
ActionScriptVoid("InitMapList");<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">function CallShowMapList()<br />
{<br />
ActionScriptVoid("ShowMapList");<br />
}</span></blockquote>Finally, create two receiver functions called by the HUD to execute our BMOpenMap() command and the Quit command:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">function OpenMap(string MapFile)<br />
{<br />
ConsoleCommand("BMOpenMap " @ MapFile);<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">function CloseMap()<br />
{<br />
ConsoleCommand("Quit");<br />
}</span></blockquote>That’s it! If everything went smoothly, hitting Esc will now pop up a menu of map choices. Clicking on a map name will transition to a new map. Clicking the close button will cleanly exit the UDK.<br />
<br />
<a href="http://www.4shared.com/file/HjyV-r8v/Transitions_Menus_and_Config_F.html">BattleMap mode source files</a>ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com14tag:blogger.com,1999:blog-2513175893262458465.post-10356149847611420182011-06-09T13:04:00.000-07:002011-06-09T13:04:46.261-07:00Taking a Second WindThere won't be a blog post addressing a new feature this coming weekend. But I didn't want to leave you without something to play with, so I'm addressing a few questions asked in the comments:<br />
<br />
Our friends at <a href="http://monsterlayer.com/">MonsterLayer.com</a> gave us permission to give away the assets they created for us. Copy <a href="http://www.4shared.com/file/cdEoPzgr/BattleMapItems.html">BattleMapItems.upk</a> to your /UDKGame/Content/Misc directory. It includes:<br />
<ul><li>Bed</li>
<li>Bedroll</li>
<li>Chair</li>
<li>Chest</li>
<li>Drawer</li>
<li>Ladder</li>
<li>Small Table</li>
<li>Table</li>
</ul>Here's a couple of clips of the BattleMap in use from our last session:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><iframe allowfullscreen='allowfullscreen' webkitallowfullscreen='webkitallowfullscreen' mozallowfullscreen='mozallowfullscreen' width='320' height='266' src='https://www.youtube.com/embed/W8a7xzNwwQY?feature=player_embedded' frameborder='0'></iframe></div><br />
And finally, according to the <a href="http://udn.epicgames.com/Three/DevelopmentKitFAQ.html">UDK Licensing FAQ</a>, as long as you're not making any money off your creation you are free to package and distribute your project.ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com3tag:blogger.com,1999:blog-2513175893262458465.post-18873208877124146622011-05-29T14:52:00.000-07:002011-05-30T10:00:29.191-07:00D&D BattleMap Using the UDK - Something Old, Something New, Something Borrowed...No new groundbreaking features this post. We’re going to spend this time tinkering with what we have a bit.<br />
<br />
First order of business is fixing BMInteractObject(), which selects (picks up and drags) placeable objects. It appears to work well, except when clicking over both a map object and a HUD object. In that case, it never lets go of the HUD object. This is caused by a small bug.<br />
<br />
Actually, the only reason it works at all is due to that bug. The code is divided into two sections: a drop and a pickup. In the drop code at the top, the switch statement checks for “CrossHairMovie” to see if the HUD has an object selected. That is incorrect, as the code checks the name of the OBJECT not the name of the CLASS. That causes the drop section to always fail for the HUD, which is actually a good thing. The code then falls to the pickup section, which calls the HUD’s InteractObject() again if a map object was not selected.<br />
<br />
If we fix the reference in the drop section, another problem appears: the cursors never lets go of any HUD object. The HUD’s InteractObject() is self-contained with its own drop and pickup code, including a special clause to ensure it doesn’t pick up the same thing it dropped. If we try to drop and pick up in two separate calls, it always picks up what it dropped.<br />
<br />
The easy answer was to move the call to the HUD’s InteractObject() to the top and always do it first. That’s less than ideal however, since HUD objects overlay map objects. We had to move a HUD object out of the way in order to click on a map object. So, the HUD’s InteractObject() call needs to be at the end. But what if we clicked on a map object? We passed a parameter to InteractObject() to only drop what’s selected and not select anything new.<br />
<br />
Then it worked! And created a new problem. If we moved a map object under a HUD object, or a HUD object directly over a map object, and clicked to let go it would hot-swap between the two. We couldn’t let go unless there was nothing else to select.<br />
<br />
The final solution is to simply do one or the other. Swapping was an interesting bit of code, but simply not feasible during gameplay. Here’s the final BMInteractObject():<br />
<br />
<strong>BattleMapPlayerInput.uc</strong><br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">exec function BMInteractObject()<br />
{<br />
local BattleMapTorch To;<br />
local Trigger Tr;<br />
local int i, j, k;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)<br />
{<br />
// stop doing whatever the currently selected object is doing<br />
if (ObjectSelected != none)<br />
{<br />
switch(ParseObjectName(ObjectSelected.Name))<br />
{<br />
case "Torch":<br />
To = BattleMapTorch(ObjectSelected);<br />
To.StopTorch(MouseOrigin);<br />
break;<br />
case "GfxHud":<br />
BattleMapHUD(myHUD).CrossHairMovie.CallInteractObject(false);<br />
break;<br />
}<br />
ObjectSelected = none;<br />
}<br />
else<br />
{<br />
//start doing something with a newly selected object<br />
switch(ParseObjectName(ObjectUnderMouse))<br />
{<br />
case "Torch":<br />
foreach DynamicActors(class'BattleMapTorch', To)<br />
if (To.Name == ObjectUnderMouse)<br />
{<br />
ObjectSelected = To;<br />
To.MoveTorch();<br />
}<br />
break;<br />
case "Trigger":<br />
// activate each Kismet event attached to this trigger<br />
foreach DynamicActors(class'Trigger', Tr)<br />
if (Tr.Name == ObjectUnderMouse)<br />
for (i=0; i<Tr.GeneratedEvents.Length; i++)<br />
for (j=0; j<Tr.GeneratedEvents[i].OutputLinks.Length; j++)<br />
for (k=0; k<Tr.GeneratedEvents[i].OutputLinks[j].Links.Length; k++)<br />
Tr.GeneratedEvents[i].OutputLinks[j].Links[k].LinkedOp.ForceActivateInput(Tr.GeneratedEvents[i].OutputLinks[j].Links[k].InputLinkIdx);<br />
break;<br />
}<br />
//if no map object found, check for a HUD object<br />
if (ObjectSelected == none)<br />
if(BattleMapHUD(myHUD).CrossHairMovie.CallInteractObject(true) > 0)<br />
ObjectSelected = BattleMapHUD(myHUD).CrossHairMovie;<br />
}<br />
}<br />
}</span></blockquote><strong>BattleMapGfxHud.uc</strong><br />
<br />
Add a parameter to CallInteractObject:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">function int CallInteractObject(bool Param1)</span></blockquote><strong>ActionScript</strong><br />
<br />
In InteractObject(), update the definition to include the new parameter:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">function InteractObject(Param1:Boolean)</span></blockquote>And preface the loop that checks for objects under the mouse with the passed parameter:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> //find new object to interact with<br />
if (Param1)<br />
for (var MousedObject in _root)</span></blockquote>Finally, update the call in DeleteObject() to include the parameter:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> InteractObject(true);</span></blockquote><div><hr />Now, let’s take what we’ve done with Zones and create map tokens for our monsters and players. Even though we place miniatures on the table during gameplay, it is still helpful to represent creatures within the battlemap for several reasons:</div><ul><li>The DM doesn’t have to move around the table to reposition minis. He can move a token with a click then ask a player (or demand, depending on the DM) to move the miniature.</li>
<li>The DM can easily see the position of creatures without moving around for a better view. I can’t tell you how many times I’ve declared an invalid opportunity attack by an NPC, sparking a bewildered debate among the players. Or missed an opportunity attack on players, all due to my skewed view from the end of the table.</li>
<li>The minis are lit by whatever texture they happen to be standing on. A white token would “spotlight” the minis, despite standing on black rock, yellow stone or green goo.</li>
</ul><div><strong>Flash</strong></div><div>Create a new token symbol within Flash. I simply made a 256x256 white circle with a black border. Don’t forget to check “Export for ActionScript”:</div><div></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWzuqrn5MMfWdceCpd34miIR0-MAOXu6k-ff6iaomgfWbom8WHdz_9D0DSQHssMqLLEx9g4Bv8MeEi-qRNYOGjj0ECd0g4V64tN6wetgwe0-JA_5g7JNyBV6JC-FXfah-AhkwFduakGB3D/s1600/token1.bmp" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="298" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWzuqrn5MMfWdceCpd34miIR0-MAOXu6k-ff6iaomgfWbom8WHdz_9D0DSQHssMqLLEx9g4Bv8MeEi-qRNYOGjj0ECd0g4V64tN6wetgwe0-JA_5g7JNyBV6JC-FXfah-AhkwFduakGB3D/s320/token1.bmp" width="320" /></a></div><div></div><div></div><div>Next, add a block of Dynamic Text with a “P1” placeholder. Give it a variable name of “TokenText”:</div><div></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZIxqo-NbZu7RdEKtrzim3BnAN_k1hMHuT0ekHnVuUTvbWaWxP_uC9qhxD3ZcggLY_5HsJ1VQGsR5QxWfst6OPIa8X-QQBMmmIf90dAuIvFEy7DC9wC5kJmMBoz5acvbVk3CDW2nrrGxcO/s1600/token2.bmp" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="298" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZIxqo-NbZu7RdEKtrzim3BnAN_k1hMHuT0ekHnVuUTvbWaWxP_uC9qhxD3ZcggLY_5HsJ1VQGsR5QxWfst6OPIa8X-QQBMmmIf90dAuIvFEy7DC9wC5kJmMBoz5acvbVk3CDW2nrrGxcO/s320/token2.bmp" width="320" /></a></div><div></div><div></div><div>You will also need to Embed the font (button next to Style). I chose only Uppercase and Numerals.</div><div><strong>ActionScript</strong></div><div>Add new global variables to the top:</div><blockquote style="border: 1px solid black;"><div><span style="font-family: "Courier New", Courier, monospace;">var DefaultZoneScale:Number = 70;</span></div><div><span style="font-family: "Courier New", Courier, monospace;">var DefaultTokenScale:Number = 50;</span></div><div><span style="font-family: "Courier New", Courier, monospace;">var Tokens:Number = 0;</span></div></blockquote>Add a “Token” case to the Spawn() function:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> case "Token":<br />
SpawnToken();<br />
break;</span></blockquote>Add the new SpawnToken() function below. It’s a separate function like SpawnZone() just to be consistent, though it didn’t need to be. SpawnZone() had additional parameters because the SwapObject() function replaced a zone by spawning a new zone with the same properties as the old one. When we swap a token, we’ll simply be changing the text.<br />
<br />
For now, we just assign a counter to the token text. When we implement a pop-up UI within the HUD for manipulating objects, we'll pass in typed text. For now, just note the P# or M# next to the creature in your combat tracker.<br />
<br />
We’re also ensuring that the cursor is always on top. Since a token will never need to remember what layer it was on (like swapping a zone) we just grab the highest available depth. That, of course, places it over the cursor so we need to bump that as well.<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">function SpawnToken()<br />
{<br />
Tokens += 1;<br />
var t1 = _root.attachMovie("TokenSymbol", "Tokn" + Tokens, _root.getNextHighestDepth());<br />
t1._x = _root._xmouse;<br />
t1._y = _root._ymouse;<br />
t1._xscale = DefaultTokenScale;<br />
t1._yscale = DefaultTokenScale;<br />
t1.TokenText = "M" + Tokens;<br />
<br />
CursorInst.swapDepths(_root.getNextHighestDepth());<br />
}</span></blockquote>Now, update the InteractObject() function so it will grab tokens as well. We want it to work exactly the same as selecting a Zone, so we simply need to update the case statement:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> case "Zone":<br />
case "Tokn":<br />
//find zones/tokens, but not the one we had clicked on</span></blockquote>The same is true for the ModifyObject() function. Modifying a token will manipulate its scale just like a zone. However, we want to implement a new feature while we’re at it. If we size a token to a 1x1 square on our battlemap, it is highly likely that additional tokens will need to be that size as well. We already used the DefaultTokenScale global variable when spawning a new token, so we need to set that value here when modifying an object:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> case "Zone":<br />
_root[ObjectSelected]._xscale *= (1 + (0.1 * Param1));<br />
_root[ObjectSelected]._yscale = _root[ObjectSelected]._xscale;<br />
DefaultZoneScale = _root[ObjectSelected]._xscale;<br />
break;<br />
case "Tokn":<br />
_root[ObjectSelected]._xscale *= (1 + (0.1 * Param1));<br />
_root[ObjectSelected]._yscale = _root[ObjectSelected]._xscale;<br />
DefaultTokenScale = _root[ObjectSelected]._xscale;<br />
break;</span></blockquote>The SwapObject() function will simply update the text on the token, alternating between “P#” (PC) and “M#” (monster):<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> case "Tokn":<br />
switch(_root[ObjectSelected].TokenText.substr(0, 1))<br />
{<br />
case "P":<br />
_root[ObjectSelected].TokenText = "M" + _root[ObjectSelected].TokenText.substr(1);<br />
break;<br />
case "M":<br />
_root[ObjectSelected].TokenText = "P" + _root[ObjectSelected].TokenText.substr(1);<br />
break;<br />
}<br />
break;</span></blockquote>Finally, let’s implement the same default scaling for zones. We've already done most of the work along the way. In SpawnZone(), change the ZoneScale variable to:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> var ZoneScale = (Param5 != undefined) ? Param5 : DefaultZoneScale;</span></blockquote><strong>BattleMapPlayerInput.uc</strong><br />
<br />
Add a new case to BMSpawn():<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> case "Token":<br />
BattleMapHUD(myHUD).CrossHairMovie.CallSpawn("Token");</span></blockquote><strong>DefaultInput.ini</strong><br />
<br />
Add a new key binding:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">.Bindings=(Name="N",Command="BMSpawn Token")</span></blockquote><hr />Let’s enhance our blood decal a bit. We’ll borrow the same movement functionality as our torch and apply it to the blood decal.<br />
<br />
<strong>BattleMapBlood.uc</strong><br />
<br />
Insert the following block into the BattleMapBlood class between the declaration and PostBeginPlay(). This code is nearly identical to the Torch code (we changed the names and removed the VerticalOffset that the torch light required):<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">var repnotify Vector BloodLoc,BloodStopLoc;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">replication<br />
{<br />
if (bNetDirty)<br />
BloodLoc,BloodStopLoc;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// called on clients when BloodStopLoc gets replicated<br />
simulated event ReplicatedEvent( name VarName )<br />
{<br />
if (VarName == nameof(BloodLoc) )<br />
SetLocation( BloodLoc );<br />
else if (VarName == nameof(BloodStopLoc) )<br />
StopBlood( BloodStopLoc );<br />
else<br />
super.ReplicatedEvent( VarName );<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// this function should be run on both clients and server<br />
simulated function MoveBlood( )<br />
{<br />
bCollideWorld = false;<br />
SetCollision(false, false);<br />
GotoState( 'BloodMoving' );<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">simulated function StopBlood( Vector NewBloodStopLoc )<br />
{<br />
bCollideWorld = true;<br />
SetCollision(true, true);<br />
GotoState('BloodStopped');</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> BloodStopLoc = NewBloodStopLoc;<br />
SetLocation(NewBloodStopLoc);<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">simulated state BloodMoving<br />
{<br />
simulated event Tick( float DeltaTime )<br />
{<br />
// move the blood...<br />
BloodLoc = BattleMapPlayerController(Owner).MouseOrigin;<br />
SetLocation(BloodLoc);<br />
}<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">simulated state BloodStopped<br />
{<br />
simulated event Tick( float DeltaTime )<br />
{<br />
// stop the blood...<br />
}<br />
}</span></blockquote><strong>BattleMapPlayerInput.uc</strong><br />
<br />
In BMInteractObject(), add a new variable:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> local BattleMapBlood Bl;</span></blockquote>Add a blood case to the “drop” switch statement:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> case "Blood":<br />
Bl = BattleMapBlood(ObjectSelected);<br />
Bl.StopBlood(MouseOrigin);<br />
break;</span></blockquote>And a blood case to the “select” switch statement:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> case "Blood":<br />
foreach DynamicActors(class'BattleMapBlood', Bl)<br />
if (Bl.Name == ObjectUnderMouse)<br />
{<br />
ObjectSelected = Bl;<br />
Bl.MoveBlood();<br />
}<br />
break;</span></blockquote>Next, we’ll borrow the same sizing functionality as our zones and tokens. Add the following new variable to the top:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">var float DefaultBloodSize;</span></blockquote>In BMModifyObject(), add a new local variable:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> local BattleMapBlood B;</span></blockquote>And a blood case to the switch statement. We’ll manipulate the decal’s size:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> case "BattleMapBlood":<br />
B = BattleMapBlood(ObjectSelected);<br />
B.Decal.Width = B.Decal.Width * (1 + (0.1 * Modifier));<br />
B.Decal.Height = B.Decal.Height * (1 + (0.1 * Modifier));<br />
DefaultBloodSize = B.Decal.Width;</span></blockquote>In BMSpawn (), add a new local variable:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> local BattleMapBlood Bl;</span></blockquote>Next, update the “Blood” case in the switch statement to resize after spawning. The call to SetLocation() is simply a dirty way to force a replication:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> case "Blood":<br />
Bl = Spawn(class'BattleMapBlood',Outer,,MouseOrigin );<br />
Bl.Decal.Width = DefaultBloodSize;<br />
Bl.Decal.Height = DefaultBloodSize;<br />
Bl.SetLocation(MouseOrigin);<br />
break;</span></blockquote>Finally, set the DefaultBloodSize in DefaultProperties:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> DefaultBloodSize = 200f</span></blockquote>Now our HUD interactivity is fixed, we have new creature tokens and we can manipulate our blood markers:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><iframe allowfullscreen='allowfullscreen' webkitallowfullscreen='webkitallowfullscreen' mozallowfullscreen='mozallowfullscreen' width='320' height='266' src='https://www.youtube.com/embed/NwDEosUw1BI?feature=player_embedded' frameborder='0'></iframe></div><br />
<br />
<a href="http://www.4shared.com/file/8c7LpMWA/Something_Old_Something_New.html">BattleMap mode source files</a>ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com5tag:blogger.com,1999:blog-2513175893262458465.post-40301604002090604932011-05-15T09:44:00.000-07:002011-05-17T07:28:23.377-07:00D&D BattleMap Using the UDK – Placeable ZonesNormally I wait until the end for the big “reveal”, but this is too cool not to show off:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><object class="BLOGGER-youtube-video" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0" data-thumbnail-src="http://2.gvt0.com/vi/2xq1UjTgu_I/0.jpg" height="266" width="320"><param name="movie" value="http://www.youtube.com/v/2xq1UjTgu_I&fs=1&source=uds" /><param name="bgcolor" value="#FFFFFF" /><embed width="320" height="266" src="http://www.youtube.com/v/2xq1UjTgu_I&fs=1&source=uds" type="application/x-shockwave-flash"></embed></object></div><br />
Our <a href="http://www.youtube.com/watch?v=eZ0x3c97mbg">first implementation of zones</a> involved creating a plane object with a scripted texture. Color and text were passed to the object which would draw the text on a canvas and apply it to a Material Instance. And of course, it had the same positioning code as the torch.<br />
<br />
However now that we’re making use of UDK’s ScaleForm UI, I thought we could pull off some pretty spectacular effects with Flash. We’re not including text (yet), but the tradeoff is worth it. Like the <a href="http://zombpir8ninja.blogspot.com/2011/03/d-battlemap-using-udk-grid.html">grid</a>, it takes a LOT less code to implement the zones in Flash.<br />
<br />
We’ll continue to use our existing BMSpawn, BMInteract, BMModify and BMDelete functions to start things off, but all the work will done in similar functions in ActionScript. Essentially, we’ll blindly “toss up” the commands to ActionScript and let it handle them with its own Spawn, Interact, Modify, etc.<br />
<br />
First, let’s update our UnrealScript functions.<br />
<br />
<strong>BattleMapGfxHud.uc</strong><br />
<br />
Add the following wrapper functions to BattleMapGfxHud. Note that most of these do not expect a return value. We’re just forwarding the command along. The UnrealScript code doesn’t care how ActionScript handles it. Except for InteractObject(), and all we need to know there is if we actually did click on something.<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">function CallSpawn(string Param1)<br />
{<br />
ActionScriptVoid("Spawn");<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">function int CallInteractObject()<br />
{<br />
local int ObjectFound;<br />
ObjectFound = ActionScriptInt("InteractObject");<br />
return ObjectFound;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">function CallModifyObject( int Param1 )<br />
{<br />
ActionScriptVoid("ModifyObject");<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">function CallDeleteObject()<br />
{<br />
ActionScriptVoid("DeleteObject");<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">function CallSwapObject( int Param1 )<br />
{<br />
ActionScriptVoid("SwapObject");<br />
}</span></blockquote><strong>BattleMapPlayerInput.uc</strong><br />
<br />
We need to extend our existing functions for new “zone” objects. In BMSpawn(), add this to our switch statement:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> case "Zone":<br />
BattleMapHUD(myHUD).CrossHairMovie.CallSpawn("Zone");<br />
break;</span></blockquote><br />
The BMInteractObject() function tracks what we selected. In this case, we don’t necessarily know the specific item since ActionScript is handling it. So, we’ll just track that our CrossHairMovie object is selected since we know something in there is. (Yes, in hindsight, “CrossHairMovie” probably wasn’t the most appropriate name for that. Note to self: fix that later…) In the first switch statement which stops selecting, add:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> case "CrossHairMovie":<br />
BattleMapHUD(myHUD).CrossHairMovie.CallInteractObject();<br />
ObjectSelected = none;<br />
break;</span></blockquote><br />
After the next switch statement that determines what we selected, add this CallInteractObject() check. If ActionScript tells us that it selected something, then we track that the CrossHairMovie has something:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> if(BattleMapHUD(myHUD).CrossHairMovie.CallInteractObject() > 0)<br />
{<br />
ObjectSelected = BattleMapHUD(myHUD).CrossHairMovie;<br />
}<br />
</span></blockquote>In the BMModifyObject() switch statement, pass the modifier along:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> case "BattleMapGfxHu":<br />
BattleMapHUD(myHUD).CrossHairMovie.CallModifyObject(Modifier);<br />
break;</span></blockquote><br />
Yes, the case parameter is correct. The switch statement checks the first 14 characters of the object type, not the name.<br />
<br />
<em>Replace</em> the entire BMDeleteObject() function with the code below. We need to add an “object deleted?” check throughout the function. Since it would be easier to click inside a large zone than a torch, we want to track if we actually deleted an UnrealScript object first so that we don’t accidently delete multiple items in both UnrealScript and ActionScript. If nothing was found, then we pass the command along:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">exec function BMDeleteObject()<br />
{<br />
local BattleMapTorch To;<br />
local BattleMapBlood Bl;<br />
local bool ObjectDeleted;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)<br />
{<br />
ObjectDeleted = false;<br />
switch(ParseObjectName(ObjectUnderMouse))<br />
{<br />
case "Torch":<br />
foreach DynamicActors(class'BattleMapTorch', To)<br />
if (To.Name == ObjectUnderMouse)<br />
{<br />
To.Destroy();<br />
ObjectDeleted = true;<br />
}<br />
break;<br />
case "Blood":<br />
foreach DynamicActors(class'BattleMapBlood', Bl)<br />
if (Bl.Name == ObjectUnderMouse)<br />
{<br />
Bl.Destroy();<br />
ObjectDeleted = true;<br />
}<br />
break;<br />
}<br />
if (!ObjectDeleted)<br />
BattleMapHUD(myHUD).CrossHairMovie.CallDeleteObject();<br />
}<br />
}</span></blockquote>Finally, add a new BMSwapObject() function. Our ModifyObject() function increases an item’s size, for example, but BMSwapObject() will exchange it for something else. Like different blood decals, or a sunrod in place of a torch, or different types of zones:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">exec function BMSwapObject(int Modifier)<br />
{<br />
if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)<br />
{<br />
if (ObjectSelected != none)<br />
{<br />
switch(Left(ObjectSelected, 14))<br />
{<br />
case "BattleMapGfxHu":<br />
BattleMapHUD(myHUD).CrossHairMovie.CallSwapObject(Modifier);<br />
break;<br />
}<br />
}<br />
}<br />
}</span></blockquote><br />
<strong>DefaultInput.ini</strong><br />
<br />
We need three more keybindings for spawning a zone and swapping an object:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">.Bindings=(Name="Z",Command="BMSpawn Zone")<br />
.Bindings=(Name="LeftBracket", Command="BMSwapObject -1")<br />
.Bindings=(Name="RightBracket", Command="BMSwapObject 1")</span></blockquote><br />
<strong>Photoshop/Gimp</strong><br />
<br />
Now, something new. The zones are three layered images rotating in different directions animated in Flash. The runic circles themselves are brushes from <a href="http://www.obsidiandawn.com/arcane-circles-photoshop-gimp-brushes">Obsidian Dawn</a>. At the bottom of their page, they provide instructions for importing and using the brushes. Essentially, we:<br />
<ul><li>Created three layers in a transparent image</li>
<li>Chose a color</li>
<li>Painted a large, medium and small runic circle on its own layer</li>
<li>Align the circles</li>
<li>Crop and resize the image to a power of 2 (256x256, 512x512, etc.)</li>
<li>Save as PNG into the .\UDKGame\Flash\BMHud\BMHud folder along with the cursor</li>
</ul><strong>Flash</strong><br />
<br />
To get the circles animating, import the individual images into Flash. Don’t forget to update the properties of each PNG file to “Lossless (PNG/GIF)” Compression, otherwise they won’t import into the UDK:<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEim7e6KuNp_j5YMkXseXcVjExCnpRxjakv1KPaV0gN50dYBHF3oZD_4P8j9bHsr7nKDhAcr3-sdE6ABVBE6MkydLJKZeh-iwPrIm0SbSejDHr7Sej7AY2E9YC3g5GXFWjCGMEr6Mq6nu288/s1600/Flash1.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="110" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEim7e6KuNp_j5YMkXseXcVjExCnpRxjakv1KPaV0gN50dYBHF3oZD_4P8j9bHsr7nKDhAcr3-sdE6ABVBE6MkydLJKZeh-iwPrIm0SbSejDHr7Sej7AY2E9YC3g5GXFWjCGMEr6Mq6nu288/s320/Flash1.jpg" width="320" /></a></div><br />
Next, create a new Symbol. Just as in Photoshop/Gimp, create three layers and put a rune image in each later. (If you lined them up correctly before cropping/saving earlier, then here you simply need to stack them perfectly on top of each other. If not, you can still make fine adjustments here.)<br />
<br />
Insert a frame way out on the timeline for however long you want the animation to run. Ours goes 360 frames, which is 15 seconds at 24 fps. We didn’t want to cause epileptic seizures with fast spinning circles. Create a motion tween on each later, setting the number of rotations and the direction:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjpekjnaSKuahXm0u3yS6g5JyNzOO3jsfioZL3b_5DmI2UpCNomJGK8Oq9shDn-SooF-gsxbjBSb7QLIoEX3NrkIxzjr9-vDKvfT57aIC6Hf1xYZfC99vdsMfcOH0UaYCQgKCGoM3lxjON_/s1600/Flash2.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="259" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjpekjnaSKuahXm0u3yS6g5JyNzOO3jsfioZL3b_5DmI2UpCNomJGK8Oq9shDn-SooF-gsxbjBSb7QLIoEX3NrkIxzjr9-vDKvfT57aIC6Hf1xYZfC99vdsMfcOH0UaYCQgKCGoM3lxjON_/s320/Flash2.jpg" width="320" /></a></div><br />
Once you’re done creating your runic circle, edit the symbol’s properties in the library:<br />
<ul><li>Enable “Export for ActionScript”</li>
<li>Enable “Export in frame 1”</li>
<li>Enter an identifier. Ours are CircleXXX. (CircleBlue, CircleGreen, CircleRed, etc.)</li>
</ul><strong>ActionScript</strong><br />
<br />
Once you’re done creating all your circles, we just need to add object manipulation functions to ActionScript. First, a simple Spawn function:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">function Spawn(Param1:String)<br />
{<br />
switch(Param1)<br />
{<br />
case "Zone":<br />
SpawnZone();<br />
break;<br />
}<br />
<br />
}</span></blockquote><br />
Next, a function to create a zone. This function will perform a double duty. It will create a brand new zone, as well as replace an existing one. It does that by creating a zone at the same level as an existing one. (Flash allows one movieclip per level.) So, the function accepts input parameters to predefine a zone’s color and shape, or sets defaults if none are provided.<br />
<br />
To keep things simple, we’re also performing some slight-of-hand with the movieclip’s properties. We need to track two things: which symbol this zone is using and what level it’s on. Rather than creating a custom class to extend movieclip for two properties, we’re going to appropriate existing properties. We’ll attach the level to the zone’s name (since both the level and the name need to be unique anyway, it seemed a good fit), and store the type of symbol in the taborder property.<br />
<br />
“Wait, what?” Yeah, that’s kinda tricky. Flash doesn’t provide a property to determine what linkage type a symbol instance was created from. So, we’ll store our linkage names in an array then save the individual index in the taborder property – also an integer value. Which as it turns out, makes it easy to swap instances since we simply need to rotate the index number:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">var Zones:Array = Array("CircleBlue", "CircleGreen", "CirclePurple", "CircleRed", "CircleYellow");</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// Param1:Name, Param2:Index, Param3:x, Param4:y, Param5:scale<br />
function SpawnZone(Param1:String, Param2:Number, Param3:Number, Param4:Number, Param5:Number)<br />
{<br />
var ZoneName = (Param1 != undefined) ? Param1 : GetNextZone();<br />
var ZoneIndex = (Param2 != undefined) ? Param2 : Math.floor(Math.random() * Zones.length);<br />
var ZoneX = (Param3 != undefined) ? Param3 : _root._xmouse;<br />
var ZoneY = (Param4 != undefined) ? Param4 : _root._ymouse;<br />
var ZoneScale = (Param5 != undefined) ? Param5 : 70;<br />
var ZoneLevel = int(ZoneName.substr(4));</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> var z1 = _root.attachMovie(Zones[ZoneIndex], ZoneName, ZoneLevel);<br />
z1._x = ZoneX;<br />
z1._y = ZoneY;<br />
z1.tabIndex = ZoneIndex;<br />
z1._xscale = ZoneScale;<br />
z1._yscale = ZoneScale;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">function GetNextZone()<br />
{<br />
var LastZone:Number = 1;<br />
for (var RootObject in _root)<br />
if (RootObject.substr(0, 4) == "Zone")<br />
LastZone = (int(RootObject.substr(4)) > LastZone) ? int(RootObject.substr(4)) : LastZone;<br />
<br />
return "Zone" + (LastZone + 1);<br />
}</span></blockquote>Implement the InteractObject() function to handle clicking an item. This works exactly like its UnrealScript counterpart. On each mouse click it first stops interacting with whatever may be currently selected, then tries to find a new item to select:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">var ObjectSelected:String;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">function InteractObject()<br />
{<br />
var PrevObjectSelected:String;<br />
<br />
//stop Interacting with existing object<br />
if (ObjectSelected.length > 0)<br />
{<br />
DragObject("");<br />
PrevObjectSelected = ObjectSelected;<br />
ObjectSelected = "";<br />
}<br />
<br />
//find new object to interact with<br />
for (var MousedObject in _root)<br />
{<br />
if (_root[MousedObject].hitTest(_root._xmouse, _root._ymouse, false))<br />
{<br />
switch (_root[MousedObject]._name.substr(0, 4))<br />
{<br />
case "Zone":<br />
//find zones, but not the one we had clicked on<br />
if (_root[MousedObject]._name != PrevObjectSelected)<br />
{<br />
ObjectSelected = _root[MousedObject]._name;<br />
}<br />
break;<br />
}<br />
}<br />
if (ObjectSelected.length > 0)<br />
{<br />
DragObject(MousedObject);<br />
return 1;<br />
}<br />
}<br />
return 0;<br />
}</span></blockquote>When moving torches in UnrealScript, we set the object to either a moving or stationary state. The moving state updated itself every tick to the mouse’s position. Here, we can let Flash do all the work. Just like our custom cursor, we tell Flash to drag around our symbol. Flash will only drag one item at a time, so we hide our cursor symbol and reattach it when we’re not dragging something else:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">function DragObject(ObjectDragging:String)<br />
{<br />
if (ObjectDragging.length > 0)<br />
{<br />
CursorInst._visible = false;<br />
startDrag(ObjectDragging);<br />
} else {<br />
CursorInst._visible = true;<br />
startDrag("CursorInst", true);<br />
}<br />
}</span></blockquote><br />
ModifyObject(), again, works just like the UnrealScript version. It determines what’s selected, then takes an appropriate action. For a zone, we modify the scale:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">function ModifyObject(Param1:Number)<br />
{<br />
if (ObjectSelected.length > 0)<br />
{<br />
switch(ObjectSelected.substr(0, 4))<br />
{<br />
case "Zone":<br />
_root[ObjectSelected]._xscale *= (1 + (0.1 * Param1));<br />
_root[ObjectSelected]._yscale = _root[ObjectSelected]._xscale;<br />
break;<br />
}<br />
}<br />
}</span></blockquote>DeleteObject() first calls InteractObject() to see if there’s something under the mouse to delete. If so, delete it, then start dragging the cursor again:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">function DeleteObject()<br />
{<br />
InteractObject();<br />
if (ObjectSelected.length > 0)<br />
{<br />
_root[ObjectSelected].removeMovieClip();<br />
DragObject();<br />
}<br />
}</span></blockquote>Finally, a new function SwapObject(). This will swap an existing zone for another. It simply records the properties of the selected zone then passes those to SpawnZone(). Since it’s inserted at the same level, Flash removes the old one for us. The modifier we pass in determines which direction we loop through the array of linkage names:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">function SwapObject(Param1:Number)<br />
{<br />
if (ObjectSelected.length > 0)<br />
{<br />
switch(ObjectSelected.substr(0, 4))<br />
{<br />
case "Zone":<br />
var ZoneID:Number = _root[ObjectSelected].tabIndex;<br />
ZoneID += Param1;<br />
ZoneID = (ZoneID >= Zones.length) ? 0 : ZoneID;<br />
ZoneID = (ZoneID < 0) ? Zones.length - 1 : ZoneID;<br />
<br />
SpawnZone(_root[ObjectSelected]._name,<br />
ZoneID,<br />
_root[ObjectSelected]._x,<br />
_root[ObjectSelected]._y,<br />
_root[ObjectSelected]._xscale);<br />
startDrag(ObjectSelected);<br />
break;<br />
}<br />
}<br />
}</span></blockquote><br />
Now, fire up the UDK Editor and reimport your BMHud SwfMovie. Make sure the package also imported all your circle images. I’m not sure when it was fixed, but the FrontEnd is now smart enough to detect the updated package so you shouldn’t have to manually copy it into the CookedPC folder.<br />
<br />
Last but not least, by request, a way to toggle the debug text.<br />
<br />
<strong>DefaultInput.ini</strong><br />
<br />
Bind a key for toggling debug mode:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">-Bindings=(Name="Q",Command="GBA_ToggleTranslocator")<br />
.Bindings=(Name="Q",Command="BMDebug")</span></blockquote><strong>BattleMapPlayerInput.uc</strong><br />
<br />
Add a function for toggling debug mode:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">exec function BMDebug()<br />
{<br />
DebugMode = !DebugMode;<br />
}</span></blockquote><strong>BattleMapPlayerController.uc</strong><br />
<br />
At the top add a declaration for a DebugMode variable:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;">var bool DebugMode;</span></blockquote><br />
<strong>BattleMapHUD.uc</strong><br />
<br />
In the PostRender() event, wrap the debug string and DrawText() in a condition:<br />
<blockquote style="border: 1px solid black;"><span style="font-family: "Courier New", Courier, monospace;"> if (bmPlayerController.DebugMode)<br />
{<br />
StringMessage = "NetMode=" @ WorldInfo.NetMode @ "\nMousePosition=" @ bmPlayerController.MousePosition.X @ "," @ bmPlayerController.MousePosition.Y @ "\nMouseOrigin=" @ mouseOrigin.X @ "," @ mouseOrigin.Y @ "," @ mouseOrigin.Z @ "\nObjectUnderMouse=" @ traceHit.Name @ "\nObjectClass=" @ traceHit.Class;<br />
//`Log(StringMessage);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> Canvas.DrawColor = GreenColor;<br />
Canvas.SetPos( 10, 10 );<br />
Canvas.DrawText( StringMessage, false, , , TextRenderInfo );<br />
}</span></blockquote>Thanks for the suggestion DrZuess.<br />
<br />
<a href="http://www.4shared.com/file/Z2ChNEgH/Placeable_Zones.html">BattleMap mode source files</a>ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com45tag:blogger.com,1999:blog-2513175893262458465.post-66916709633334994742011-05-01T18:53:00.000-07:002011-05-02T17:58:11.741-07:00D&D BattleMap Using the UDK - Special EffectsOur battlemap is working fairly well at this point. But unless our heroes happen upon one of our prescripted triggers, it's pretty static. Let's add some special effects that we can toss out during combat for fun.<br />
<br />
We're going to expand on what we learned about creating torches (extending an existing game object) and extend our own objects.<br />
<br />
<strong>BattleMapEmitterEffect.uc</strong><br />
<br />
Let's start with a basic fireball.<br />
<br />
Our torch was based on the EmitterSpawnable object, and extended to include movement and interactivity. This is a similar object extended from EmitterSpawnable, but we're only adding two features: a self-destruct timer (we only want it alive for a few seconds) and a camera shake. However, we've also made many of the values parameters so that they can be extended:<br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">class BattleMapEmitterEffect extends EmitterSpawnable;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">var float EffectLifespan;<br />
var float EffectStarted;<br />
var float ShakeScale;<br />
var float OscillationDur;<br />
var float PitchAmp;<br />
var float PitchFreq;<br />
var float YawAmp;<br />
var float YawFreq;<br />
var float RollAmp;<br />
var float RollFreq;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">simulated function PostBeginPlay()<br />
{<br />
EffectStarted = WorldInfo.TimeSeconds;<br />
DoCameraEffects();<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">function Tick( float DeltaTime )<br />
{<br />
if ((WorldInfo.TimeSeconds - EffectStarted) > EffectLifespan)<br />
self.Destroy();<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">function DoCameraEffects()<br />
{<br />
local CameraShake Shake;<br />
local BattleMapPlayerController PC;<br />
<br />
foreach WorldInfo.LocalPlayerControllers(class'BattleMapPlayerController', PC)<br />
{<br />
if (PC.PlayerCamera != None)<br />
{<br />
Shake = new class'CameraShake';<br />
Shake.OscillationDuration = OscillationDur;<br />
Shake.RotOscillation.Pitch.Amplitude = PitchAmp;<br />
Shake.RotOscillation.Pitch.Frequency = PitchFreq;<br />
Shake.RotOscillation.Yaw.Amplitude = YawAmp;<br />
Shake.RotOscillation.Yaw.Frequency = YawFreq;<br />
Shake.RotOscillation.Roll.Amplitude = RollAmp;<br />
Shake.RotOscillation.Roll.Frequency = RollFreq;<br />
PC.ClientPlayCameraShake(Shake, ShakeScale, false);<br />
}<br />
}<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">DefaultProperties<br />
{<br />
EffectLifespan=3.0<br />
ShakeScale=1.0<br />
OscillationDur=1.0<br />
PitchAmp=20.0<br />
PitchFreq=40.0<br />
YawAmp=20.0<br />
YawFreq=30.0<br />
RollAmp=20.0<br />
RollFreq=50.0<br />
<br />
DrawScale=3.000000</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> Begin Object Name=ParticleSystemComponent0<br />
Template=ParticleSystem'Envy_Effects.Particles.P_VH_Gib_Explosion'<br />
ReplacementPrimitive=None<br />
LightingChannels=(bInitialized=True,Dynamic=True)<br />
LODLevel=1<br />
End Object<br />
ParticleSystemComponent=ParticleSystemComponent0</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> RemoteRole=ROLE_SimulatedProxy<br />
bAlwaysRelevant=true<br />
bReplicateMovement=true<br />
bNetTemporary=true<br />
bNoDelete=false<br />
}</span></blockquote><br />
When we spawn this with a keystroke, it will throw down a screen shaking fireball wherever we're pointing.<br />
<br />
With this as a base, creating other effects is simple. We extend our own object and swap out the particle system template. Below are three more effects: a force ball, a lightning bolt, and for the ranger in our group who fires Tomahawk cruise missiles instead of arrows, an earth-shattering nuclear explosion.<br />
<br />
<strong>BattleMapEmitterEffectForce.uc</strong><br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">class BattleMapEmitterEffectForce extends BattleMapEmitterEffect;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">DefaultProperties<br />
{<br />
DrawScale=1.000000</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> Begin Object Name=ParticleSystemComponent0<br />
Template=ParticleSystem'P_WP_ShockRifle_Explo'<br />
End Object<br />
}</span></blockquote><strong>BattleMapEmitterEffectLightning.uc</strong><br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">class BattleMapEmitterEffectLightning extends BattleMapEmitterEffect;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">DefaultProperties<br />
{<br />
DrawScale=2.000000<br />
Rotation=(Pitch=16384,Yaw=0,Roll=0)<br />
<br />
Begin Object Name=ParticleSystemComponent0<br />
Template=ParticleSystem'PS_Scorpion_Gun_Impact'<br />
End Object<br />
}</span></blockquote><strong>BattleMapEmitterEffectNuke.uc</strong><br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">class BattleMapEmitterEffectNuke extends BattleMapEmitterEffect;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">DefaultProperties<br />
{<br />
PitchAmp=150.0<br />
PitchFreq=40.0<br />
YawAmp=75.0<br />
YawFreq=30.0<br />
RollAmp=150.0<br />
RollFreq=50.0<br />
<br />
Begin Object Name=ParticleSystemComponent0<br />
Template=ParticleSystem'P_VH_Death_SpecialCase_1_Base_Near'<br />
End Object<br />
}</span></blockquote><br />
<strong>BattleMapPlayerInput.uc</strong><br />
<br />
We already have a function for spawning objects (so far, just the torch) so we just need to add our new effects to BMSpawn():<br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">exec function BMSpawn(string ObjectToSpawn)<br />
{<br />
if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)<br />
{<br />
switch(ObjectToSpawn)<br />
{<br />
case "Torch":<br />
Spawn(class'BattleMapTorch',Outer,,MouseOrigin,,,true );<br />
break;<br />
case "Fireball":<br />
Spawn(class'BattleMapEmitterEffect',Outer,,MouseOrigin );<br />
break;<br />
case "Force":<br />
Spawn(class'BattleMapEmitterEffectForce',Outer,,MouseOrigin );<br />
break;<br />
case "Lightning":<br />
Spawn(class'BattleMapEmitterEffectLightning',Outer,,MouseOrigin );<br />
break;<br />
case "Nuke":<br />
Spawn(class'BattleMapEmitterEffectNuke',Outer,,MouseOrigin );<br />
break;<br />
}<br />
}<br />
}</span></blockquote><strong>DefaultInput.ini</strong><br />
<br />
Finally, bind a key to the spawn function just like we did the torch:<br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">-Bindings=(Name="F1",Command="GBA_ShowScores")<br />
-Bindings=(Name="F2",Command="GBA_ShowMap")<br />
-Bindings=(Name="F3",Command="GBA_ToggleMinimap")<br />
.Bindings=(Name="F1",Command="BMSpawn Fireball")<br />
.Bindings=(Name="F2",Command="BMSpawn Force")<br />
.Bindings=(Name="F3",Command="BMSpawn Lightning")<br />
.Bindings=(Name="F4",Command="BMSpawn Nuke")</span></blockquote><br />
<strong>BattleMapBlood.uc</strong><br />
<br />
We also need a way to mark creatures that have been "bloodied" during battle. The UDK has the perfect system for this: Decals. We'll create our own Decal actor to "spray" blood on the map. But first, we need some blood.<br />
<br />
For simplicity's sake, I made a copy of the Bio_Splat_Decal_001 DecalMaterial (I liked the "wet" effect) and saved it to my BattleMapAssets package. Then I inserted a "Multiply" at the end to make it red instead of green:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgU1GYsCIw8yczF2Wu_8Xpc3kEiV5BHcrZj8Uk1FFpPUWKY9mBlD1ngA41e7-l_0qDQNWhXPUP98H7AOaVNOKy7O66W4fCb4rLHuHA0ylXhjrT6OibFcGX5qQ2dtHyJO6YzgwZ1OC-SolCM/s1600/Blood_Decal.bmp" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="216" j8="true" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgU1GYsCIw8yczF2Wu_8Xpc3kEiV5BHcrZj8Uk1FFpPUWKY9mBlD1ngA41e7-l_0qDQNWhXPUP98H7AOaVNOKy7O66W4fCb4rLHuHA0ylXhjrT6OibFcGX5qQ2dtHyJO6YzgwZ1OC-SolCM/s320/Blood_Decal.bmp" width="320" /></a></div><br />
Our extended DecalActorMovable object below has two extra features: it ensures the decal is always sprayed straight "down", and it adds a collision component so that we can detect it with the mouse:<br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">class BattleMapBlood extends DecalActorMovable;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">simulated function PostBeginPlay()<br />
{<br />
local Rotator newRot;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> newRot.Pitch = -16384;<br />
newRot.Yaw = Rand(65536);<br />
newRot.Roll = -65536;<br />
SetRotation(newRot);<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">DefaultProperties<br />
{<br />
Begin Object Name=NewDecalComponent<br />
DecalMaterial=DecalMaterial'BattleMapAssets.Materials.Blood_Decal'<br />
End Object</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> Begin Object Class=CylinderComponent Name=CollisionCylinder<br />
CollisionRadius=+0064.000000<br />
CollisionHeight=+0064.000000<br />
BlockNonZeroExtent=true<br />
BlockZeroExtent=true<br />
BlockActors=true<br />
CollideActors=true<br />
End Object<br />
CollisionComponent=CollisionCylinder<br />
Components.Add(CollisionCylinder)</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> RemoteRole=ROLE_SimulatedProxy<br />
bAlwaysRelevant=true<br />
bReplicateMovement=true<br />
bNetTemporary=false<br />
bCanBeDamaged = false<br />
bCollideActors = true<br />
bBlockActors = true<br />
bCollideWorld = false<br />
bStatic=false<br />
bNoDelete=false<br />
bMovable=true<br />
}</span></blockquote><br />
Since we're only using a single decal, the PostBeginPlay() function also randomly rotates the blood so that the same splat doesn't look mirrored all over the battle field. You could also randomly swap out the Material if you had several blood textures.<br />
<br />
Don't forget to add the reference to our BattleMapPlayerInput.BMSpawn() function:<br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;"> case "Blood":<br />
Spawn(class'BattleMapBlood',Outer,,MouseOrigin );<br />
break;</span></blockquote>And bind a key to it in DefaultInput.ini:<br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">-Bindings=(Name="B",Command="GBA_ToggleSpeaking")<br />
.Bindings=(Name="B",Command="BMSpawn Blood")</span></blockquote><strong>BattleMapPlayerInput.uc</strong><br />
<br />
To wrap it up, we need a way to clean up our mess and delete torches and blood that we've spawned. We already know how to determine what object we're pointing at, so we just call that object's Destroy() function. Add the following function to BattleMapPlayerInput:<br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">exec function BMDeleteObject()<br />
{<br />
local BattleMapTorch To;<br />
local BattleMapBlood Bl;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)<br />
{<br />
switch(ParseObjectName(ObjectUnderMouse))<br />
{<br />
case "Torch":<br />
foreach DynamicActors(class'BattleMapTorch', To)<br />
if (To.Name == ObjectUnderMouse)<br />
{<br />
To.Destroy();<br />
}<br />
break;<br />
case "Blood":<br />
foreach DynamicActors(class'BattleMapBlood', Bl)<br />
if (Bl.Name == ObjectUnderMouse)<br />
{<br />
Bl.Destroy();<br />
}<br />
break;<br />
}<br />
}<br />
}</span></blockquote>Add bind a key to BMDeleteObject() in DefaultInput.ini:<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">.Bindings=(Name="Delete",Command="BMDeleteObject")</span></blockquote>Now, you can litter the battlefield with torches, blood and explosions while snuffing out your player's precious light sources.<br />
<br />
<br />
<a href="http://www.4shared.com/file/Mlqd1fXu/Special_Effects.html
">BattleMap mode source files</a>ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com10tag:blogger.com,1999:blog-2513175893262458465.post-28635728793717957632011-04-17T09:38:00.000-07:002011-04-17T09:38:25.350-07:00D&D BattleMap Using the UDK - Torches and LightingThis post, I eagerly wanted to get torches back into our BattleMap. The iconic dungeon torch is oddly comforting for players. Telling them "you can see 5 squares from where you stand" and actually having a lit torch on the map are two different things. You'd be surprised to find that they'll huddle around a torch bearer when they can actually see the range of their vision.<br />
<br />
The first time we tried to implement this we made the torch radius configurable through the command console. Not so great for trying to setup a candle or a sunrod, while not affecting every other torch on the map. This time, we're going to add mouse selection and keyboard commands to modify individual torches.<br />
<br />
<strong>BattleMapTorch.uc</strong><br />
<br />
The first thing we need is an object that looks like fire. I simply followed the <a href="http://udn.epicgames.com/Three/VideoTutorials.html">3D Buzz Video Tutorials</a> on particle emitters. Literally, click-for-click. The only thing I did different was turn off the smoke, since it would obscure the map from our top-down view.<br />
<br />
Once you have the emitter built, that becomes the base for our torch object. But, we need to add some features to it. Drop one onto a map, then open the properties window. There's a nifty little icon in the upper right hand corner that looks like a wrench. Choose "Copy To Clipboard", then paste the contents into an editor. What you see are the properties that would be set in the DefaultProperties section of an emitter.uc object. That's what we'll start with. (Not all of those properties are necessary, but it was certainly easier to start with that than trying to figure them all out from scratch.)<br />
<br />
Next, we add some network replication functionality. The BattleMap does work with multiple clients, however we found that using <a href="http://www.google.com/url?sa=t&source=web&cd=1&ved=0CBQQFjAA&url=http%3A%2F%2Fwww.inputdirector.com%2F&ei=GAarTduvD4fy0gHVi8H5CA&usg=AFQjCNE9xjGJnCXrNxkUXD5OhlbfbD7zkQ&sig2=3j_r-TBQkRvlZOnKwZADMQ">InputDirector</a> is easier. (See the post on <a href="http://zombpir8ninja.blogspot.com/2010/12/d-battlemap-using-udk-setup.html">the setup</a>.)<br />
<br />
Moving the torch is simply a matter of switching between two states: one that updates the torch's location from the mouse position and one that doesn't. We turn off collision detection when we're moving it around to avoid it getting stuck on walls or terrain. Why does stopping the torch also include updating the location? That has to do with network replication. For efficiency, synchronization information is not always transmitted. Some packets are skipped. (That's why you can be aiming right at someone's head but still miss a shot.) So, when the torch is no longer moving it may actually appear to be in different locations on clients. This forces every copy of the torch to be in exactly the same spot.<br />
<br />
Lastly, we add a LightAttachment component to the torch to actually create the torch light. Otherwise, it would just be a flame in the dark.<br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">class BattleMapTorch extends EmitterSpawnable;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">var Color LightColor;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">var float LightScale;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">var PointLightComponent LightAttachment;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">var Vector VerticalOffset;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">var repnotify Vector TorchLoc,TorchStopLoc;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">var float TorchRadius;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">replication</span><br />
<span style="font-family: "Courier New", Courier, monospace;">{</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if (bNetDirty)</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> TorchLoc,TorchStopLoc;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// called on clients when TorchStopLoc gets replicated</span><br />
<span style="font-family: "Courier New", Courier, monospace;">simulated event ReplicatedEvent( name VarName )</span><br />
<span style="font-family: "Courier New", Courier, monospace;">{</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if (VarName == nameof(TorchLoc) )</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> SetLocation( TorchLoc );</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> else if (VarName == nameof(TorchStopLoc) )</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> StopTorch( TorchStopLoc );</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> else</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> super.ReplicatedEvent( VarName );</span><br />
<span style="font-family: "Courier New", Courier, monospace;">}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// this function should be run on both clients and server</span><br />
<span style="font-family: "Courier New", Courier, monospace;">simulated function MoveTorch( )</span><br />
<span style="font-family: "Courier New", Courier, monospace;">{</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> bCollideWorld = false;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> SetCollision(false, false);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> GotoState( 'TorchMoving' );</span><br />
<span style="font-family: "Courier New", Courier, monospace;">}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">simulated function StopTorch( Vector NewTorchStopLoc )</span><br />
<span style="font-family: "Courier New", Courier, monospace;">{</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> bCollideWorld = true;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> SetCollision(true, true);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> GotoState('TorchStopped');</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> TorchStopLoc = NewTorchStopLoc;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> SetLocation(NewTorchStopLoc);</span><br />
<span style="font-family: "Courier New", Courier, monospace;">}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">simulated state TorchMoving</span><br />
<span style="font-family: "Courier New", Courier, monospace;">{</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> simulated event Tick( float DeltaTime )</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> {</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> // move the torch...</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> TorchLoc = BattleMapPlayerController(Owner).MouseOrigin + VerticalOffset;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> SetLocation(TorchLoc);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> }</span><br />
<span style="font-family: "Courier New", Courier, monospace;">}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">simulated state TorchStopped</span><br />
<span style="font-family: "Courier New", Courier, monospace;">{</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> simulated event Tick( float DeltaTime )</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> {</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> // stop the torch...</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> }</span><br />
<span style="font-family: "Courier New", Courier, monospace;">}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">simulated function PostBeginPlay()</span><br />
<span style="font-family: "Courier New", Courier, monospace;">{</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> LightAttachment = new(self)class'PointLightComponent';</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> LightAttachment.SetLightProperties(LightScale, LightColor);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> LightAttachment.SetEnabled(true);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> LightAttachment.Radius = TorchRadius;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> LightAttachment.SetTranslation(vect(0, 0, 1) * 50);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if ( LightAttachment != None )</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> {</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> self.AttachComponent(LightAttachment);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> }</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> super.PostbeginPlay();</span><br />
<span style="font-family: "Courier New", Courier, monospace;">}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">DefaultProperties</span><br />
<span style="font-family: "Courier New", Courier, monospace;">{</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> TorchRadius=600</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> VerticalOffset=(X=0,Y=0,Z=50)</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> LightColor=(B=0,G=200,R=255,A=255)</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> LightScale=4.0</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> DrawScale=2.000000</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> Begin Object Name=ParticleSystemComponent0</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> Template=ParticleSystem'BattleMapAssets.Particles.PS_Torch'</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> ReplacementPrimitive=None</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> LightingChannels=(bInitialized=True,Dynamic=True)</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> SecondsBeforeInactive=0</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> End Object</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> ParticleSystemComponent=ParticleSystemComponent0</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> Begin Object Class=CylinderComponent Name=CollisionCylinder</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> CollisionRadius=+0064.000000</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> CollisionHeight=+0064.000000</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> BlockNonZeroExtent=true</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> BlockZeroExtent=true</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> BlockActors=true</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> CollideActors=true</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> End Object</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> CollisionComponent=CollisionCylinder</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> Components.Add(CollisionCylinder)</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> RemoteRole=ROLE_SimulatedProxy</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> bAlwaysRelevant=true</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> bReplicateMovement=true</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> bNetTemporary=false</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> bCanBeDamaged = false</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> bCollideActors = true</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> bBlockActors = true</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> bCollideWorld = true</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> bStatic=false</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> bNoDelete=false</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> bMovable=true</span><br />
<span style="font-family: "Courier New", Courier, monospace;">}</span></blockquote><br />
<strong>BattleMapPlayerInput.uc</strong><br />
<br />
Now, we need to add functions to our PlayerInput object to manipulate torches. First is a method to spawn a torch. We created an all-purpose spawn function, and will use keybind parameters to pass in what to spawn:<br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">exec function BMSpawn(string ObjectToSpawn)<br />
{<br />
if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)<br />
{<br />
switch(ObjectToSpawn)<br />
{<br />
case "Torch":<br />
Spawn(class'BattleMapTorch',Outer,,MouseOrigin,,,true );<br />
break;<br />
}<br />
}<br />
}</span></blockquote>Next, we want to activate functionality of an object when it's selected. In this case, we want the torch to start moving when it's clicked. But each object may do something different. For example, the triggers we implemented earlier will simply "turn on" -- nothing to continue doing. So, the function will first stop whatever a currently selected object may be doing (stop moving if we had already selected a torch) then start whatever the newly selected object needs to do. <em>Replace</em> the BMInteractObject() function with the following:<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">function string ParseObjectName(Name ObjectName)<br />
{<br />
local string ObjectType;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> ObjectType = Repl(ObjectName, "BattleMap", "");<br />
if (InStr(ObjectType, "_") > -1)<br />
ObjectType = Mid(ObjectType, 0, InStr(ObjectType, "_"));<br />
return ObjectType;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">exec function BMInteractObject()<br />
{<br />
local BattleMapTorch To;<br />
local Trigger Tr;<br />
local int i, j, k;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)<br />
{<br />
// stop doing whatever the currently selected object is doing<br />
if (ObjectSelected != none)<br />
{<br />
switch(ParseObjectName(ObjectSelected.Name))<br />
{<br />
case "Torch":<br />
To = BattleMapTorch(ObjectSelected);<br />
To.StopTorch(MouseOrigin);<br />
ObjectSelected = none;<br />
break;<br />
}<br />
}<br />
//start doing something with a newly selected object<br />
switch(ParseObjectName(ObjectUnderMouse))<br />
{<br />
case "Torch":<br />
foreach DynamicActors(class'BattleMapTorch', To)<br />
if (To.Name == ObjectUnderMouse)<br />
{<br />
ObjectSelected = To;<br />
To.MoveTorch();<br />
}<br />
break;<br />
case "Trigger":<br />
// activate each Kismet event attached to this trigger<br />
foreach DynamicActors(class'Trigger', Tr)<br />
if (Tr.Name == ObjectUnderMouse)<br />
for (i=0; i<Tr.GeneratedEvents.Length; i++)<br />
for (j=0; j<Tr.GeneratedEvents[i].OutputLinks.Length; j++)<br />
for (k=0; k<Tr.GeneratedEvents[i].OutputLinks[j].Links.Length; k++)<br />
Tr.GeneratedEvents[i].OutputLinks[j].Links[k].LinkedOp.ForceActivateInput(Tr.GeneratedEvents[i].OutputLinks[j].Links[k].InputLinkIdx);<br />
break;<br />
}<br />
}<br />
}</span></blockquote>As we add more interactive objects to our BattleMap, we'll insert the method to trigger them in this function. Also, add this declaration to the top of the code along with GridZoomDelta:<br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">var Object ObjectSelected;</span></blockquote>Finally, once we have a torch selected we have the ability to modify just that instance of the object. In this case, we want to be able to modify its light radius. Another frustrating issue we had recently, due to the summer time change, was seeing the projected map in a lit room. Even with the lights off, it can be difficult to see what you <em>thought</em> was a brightly lit map in the editor. So, we wanted the ability to adjust the map's brightness. Therefore if no object is selected, the default behavior will be to manipulate the lights in a map. Note that this only works for dynamic lights. Static lights are... static. Add the following function:<br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">exec function BMModifyObject(int Modifier)<br />
{<br />
local BattleMapTorch T;<br />
local PointLightToggleable L;<br />
local LightComponent C;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)<br />
{<br />
if (ObjectSelected != none)<br />
{<br />
switch(Left(ObjectSelected, 14))<br />
{<br />
case "BattleMapTorch":<br />
T = BattleMapTorch(ObjectSelected);<br />
T.LightAttachment.Radius = T.LightAttachment.Radius * (1 + (0.1 * Modifier));<br />
break;<br />
}<br />
}<br />
else<br />
{<br />
foreach AllActors(class'PointLightToggleable', L)<br />
{<br />
C = L.LightComponent;<br />
C.SetLightProperties(C.Brightness * (1 + (0.25 * Modifier)));<br />
}<br />
}<br />
}<br />
}</span></blockquote><strong>DefaultInput.ini</strong><br />
<br />
All that's left is to bind keys to the functions we created. A "T" will spawn a torch. Left clicking will interact with objects. And the plus/minus keys ("Equals"/"Underscore") will manipulate selected objects. Replace the "I" binding in our BattleMap Bindings section with the following:<br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">.Bindings=(Name="T",Command="BMSpawn Torch")<br />
.Bindings=(Name="LeftMouseButton",Command="BMInteractObject")<br />
.Bindings=(Name="Equals",Command="BMModifyObject 1")<br />
.Bindings=(Name="Underscore",Command="BMModifyObject -1")</span></blockquote><br />
Here's what it looks like in action:<br />
<br />
<iframe allowfullscreen="" frameborder="0" height="390" src="http://www.youtube.com/embed/_hKHD7-uGeU" title="YouTube video player" width="480"></iframe><br />
<br />
<a href="http://www.4shared.com/file/xv2EG4L_/Torches_and_Lighting.html">BattleMap mode source files</a>ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com32tag:blogger.com,1999:blog-2513175893262458465.post-3731070716229358632011-04-05T19:56:00.000-07:002011-04-05T19:56:09.747-07:00D&D BattleMap Using the UDK - Making It InteractiveSo far, we're able to view a UDK map top-down and overlay it with a grid. That's great for replacing hand drawn or tiled maps - we can show a tremendous amount of detail. But to really impress our players, we want to be able to <em>move</em> objects on an interactive map. Fortunately, the UDK provides that capability with Kismet and Matinee. However, we have two problems.<br />
<br />
Once we position a room and overlay the grid, we don't want to have to move around to activate triggers. (Remember, positioning the camera actually involves moving our invisible avatar.) Besides, our pawn is an empty mesh with no Collision Model so it wouldn't activate triggers anyway. What we need is the ability to trigger actions with keyboard input.<br />
<br />
There are probably a dozen or more ways to accomplish this, but one goal we had from the beginning was to keep the map and script separate. We could easily script our own Kismet actions, but then the map builder would have to have our scripts installed beforehand to use its custom actions. Not the ideal solution. We want to be able to have anyone make any map, then launch it with our scripts to turn it into a D&D battlemap. So, we're going to try to use default Kismet triggers.<br />
<br />
<strong>BattleMapPlayerController.uc</strong><br />
<br />
First, we're going to add a new variable to our PlayerController to store what the mouse cursor is floating over. The functionality we're about to implement could all be done in the PlayerInput object when a key is pressed, but as long as we're tracing the mouse position every tick anyway we might as well store what our trace hit so we can use it later and save ourselves some cycles. Add this to the top:<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">var name ObjectUnderMouse;</span></blockquote>Then update the MouseOrigin functions so that the HUD can pass in the object that it traced:<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">simulated function SetMouseOrigin(Vector NewMouseOrigin, name NewObjectUnderMouse)<br />
{<br />
// avoid spamming if the cursor isn't moving<br />
if (MouseOrigin != NewMouseOrigin)<br />
{<br />
MouseOrigin = NewMouseOrigin;<br />
ObjectUnderMouse = NewObjectUnderMouse;<br />
ServerMouseOrigin(NewMouseOrigin, NewObjectUnderMouse);<br />
}<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">reliable server function ServerMouseOrigin(Vector NewMouseOrigin, name NewObjectUnderMouse)<br />
{<br />
MouseOrigin = NewMouseOrigin;<br />
ObjectUnderMouse = NewObjectUnderMouse;<br />
}</span></blockquote><strong>BattleMapHUD.uc</strong><br />
<br />
Replace the PostRender event with the following. We're now capturing the actor that was hit by the trace and passing it to our PlayerController. If nothing was hit, we just pass it the base WorldInfo so we don't have to test for nulls.<br />
<br />
(We also did an additional bit of cleanup here and fixed the starting point of the trace since we changed our Camera object last blog post.)<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">event PostRender()<br />
{<br />
local BattleMapPlayerController bmPlayerController;<br />
local vector startTrace;<br />
local Vector endTrace;<br />
local Vector mouseOrigin;<br />
local Vector mouseDirection;<br />
local string StringMessage;<br />
local TraceHitInfo hitInfo;<br />
local Actor traceHit;<br />
<br />
super.PostRender();</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> StringMessage = "";<br />
bmPlayerController = BattleMapPlayerController(PlayerOwner);<br />
bmPlayerController.MousePosition = GetMouseCoordinates();<br />
//Deproject the mouse from screen coordinate to world coordinate and store World Origin and Dir.<br />
Canvas.DeProject(bmPlayerController.MousePosition, mouseOrigin, mouseDirection);<br />
if (bmPlayerController.Pawn != none)<br />
{<br />
startTrace = bmPlayerController.Pawn.Location;<br />
startTrace.Z += BattleMapCamera(bmPlayerController.PlayerCamera).CameraZOffset;<br />
endTrace = startTrace + (mouseDirection * 20000);<br />
traceHit = Trace(mouseOrigin, mouseDirection, endTrace, startTrace, true, , hitInfo);<br />
}<br />
if (traceHit == none)<br />
traceHit = WorldInfo;<br />
// save final mouse 3d position (on ground)<br />
bmPlayerController.SetMouseOrigin(mouseOrigin, traceHit.Name);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> StringMessage = "NetMode=" @ WorldInfo.NetMode @ "\nMousePosition=" @ bmPlayerController.MousePosition.X @ "," @ bmPlayerController.MousePosition.Y @ "\nMouseOrigin=" @ mouseOrigin.X @ "," @ mouseOrigin.Y @ "," @ mouseOrigin.Z @ "\nObjectUnderMouse=" @ traceHit.Name @ "\nObjectClass=" @ traceHit.Class;<br />
//`Log(StringMessage);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> Canvas.DrawColor = GreenColor;<br />
Canvas.SetPos( 10, 10 );<br />
Canvas.DrawText( StringMessage, false, , , TextRenderInfo );<br />
}</span></blockquote><strong>BattleMapPlayerInput.uc</strong><br />
<br />
This is where the magic happens and is what keeps us from having to create custom Kismet actions. Add the following function to our PlayerInput object. This will get called when a key is pressed. Essentially, this function loops through all the Trigger actors in the map and finds the one we moused over. Then it manually activates all the actions linked to the Trigger. This gives us a tremendous amount of flexibility.<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">exec function BMInteractObject()<br />
{<br />
local Trigger T;<br />
local int i, j, k;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> // activate each Kismet event attached to this trigger<br />
if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)<br />
foreach DynamicActors(class'Trigger', T)<br />
if (T.Name == ObjectUnderMouse)<br />
for (i=0; i<T.GeneratedEvents.Length; i++)<br />
for (j=0; j<T.GeneratedEvents[i].OutputLinks.Length; j++)<br />
for (k=0; k<T.GeneratedEvents[i].OutputLinks[j].Links.Length; k++)<br />
T.GeneratedEvents[i].OutputLinks[j].Links[k].LinkedOp.ForceActivateInput(T.GeneratedEvents[i].OutputLinks[j].Links[k].InputLinkIdx);<br />
}</span></blockquote><br />
<strong>DefaultInput.ini</strong><br />
<br />
Finally, we just need to connect our function with a key. In the BattleMap Bindings section, add these two lines to make the "i" key interact with the map:<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">.Bindings=(Name="BMInteractObject",Command="BMInteractObject")</span><br />
<span style="font-family: "Courier New", Courier, monospace;">.Bindings=(Name="I",Command="BMInteractObject")</span></blockquote><br />
Now, anything you can animate in your map using Kismet and Matinee can be triggered on command. Things like opening doors, turning on lights, activating traps, etc.<br />
<br />
This shows how quickly and easily you can whip together a map:<br />
<br />
<iframe allowfullscreen="" frameborder="0" height="390" src="http://www.youtube.com/embed/ANMslbgyZ8c" title="YouTube video player" width="480"></iframe><br />
<br />
<a href="http://www.4shared.com/file/pT0X9AhN/Making_It_Interactive.html">BattleMap mode source files</a>ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com2tag:blogger.com,1999:blog-2513175893262458465.post-30771881752892703992011-03-26T19:14:00.000-07:002011-03-26T19:14:15.549-07:00D&D BattleMap Using the UDK - More Dungeon Less UTNow that we have an idea of how to setup a top-down camera and a grid overlay, it's time to clean things up and drop the Unreal Tournament functionality. We don't need an avatar running around while we're displaying a map and we want to be able to move independently of CSG and meshes in the level.<br />
<br />
Make a backup copy of the files we've created so far. You'll have to save the copy outside the /Development/Src directory, otherwise the FrontEnd will try to compile them as well.<br />
<br />
I'll outline the changes below if you want to follow along. Otherwise, links to archives of both versions are below.<br />
<br />
<strong>BattleMapMain.uc</strong><br />
<br />
All we're doing here is changing the object we're extending from, to the base GameInfo class. We'll no longer be playing an Unreal Tournament match when we start a level:<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">class BattleMapMain extends GameInfo;<br />
<br />
...</span></blockquote><strong>BattleMapPawn.uc</strong><br />
<br />
Changing the base class again, as well as dropping the first person mesh fix (since there won't be any) and supplying an empty pawn. We also set a few properties to enable the pawn to move freely around the level. Essentially, we put the pawn in "flying" mode instead of "walking" mode. That allows us to move vertically as well as horizontally, and effectively zooms and pans the level.<br />
<br />
Replace the <em>entire</em> BattleMapPawn.uc code with the following:<br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">class BattleMapPawn extends Pawn;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">DefaultProperties<br />
{<br />
//this is just an empty skeletal mesh to prevent log errors<br />
Begin Object Class=SkeletalMeshComponent Name=WPawnSkeletalMeshComponent<br />
End Object<br />
Mesh=WPawnSkeletalMeshComponent<br />
Components.Add(WPawnSkeletalMeshComponent)<br />
<br />
bCollideWorld=false<br />
bCollideActors=false<br />
WalkingPhysics=PHYS_Flying<br />
LandMovementState=PlayerFlying<br />
AirSpeed=+01200.000000<br />
}</span></blockquote><strong>BattleMapHUD.uc</strong><br />
<br />
All we're doing here is removing the laser sight code. <em>Delete</em> the following three lines:<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">// laser sight from pawn to reticle<br />
bmPlayerController.Pawn.GetActorEyesViewPoint(pawnEyeLocation, pawnEyeRotator);<br />
Draw3DLine(pawnEyeLocation, mouseOrigin, RedColor);</span></blockquote><strong>BattleMapPlayerController.uc</strong><br />
<br />
A lot of changes here. First, change the base object:<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">class BattleMapPlayerController extends PlayerController<br />
config(Game);</span></blockquote>Delete the following blocks. These manipulated the behavior of the UT pawn:<br />
<span style="font-family: "Courier New", Courier, monospace;"></span><br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">// This basically locks the camera to the pawn's location, and adds CameraZOffset to the Z of the camera.<br />
simulated event GetPlayerViewPoint( out vector out_Location, out Rotator out_Rotation )<br />
{<br />
...<br />
}<br />
<br />
// Force WASD to be NWSE, so use world rotation instead of pawn rotation<br />
state PlayerWalking<br />
{<br />
...<br />
}<br />
<br />
// This makes our weapons aim to the pawn's rotation, not the controller's rotation<br />
function Rotator GetAdjustedAimFor(Weapon W, vector StartFireLoc)<br />
{<br />
...<br />
}<br />
<br />
// Turn the pawn toward the cursor<br />
function UpdateRotation( float DeltaTime )<br />
{<br />
...<br />
}</span></blockquote>Add the following block. This handles the "flying" mode that we enabled in BattleMapPawn.uc:<br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">// Force WASD to be NWSE, so use world rotation instead of pawn rotation<br />
state PlayerFlying<br />
{<br />
function PlayerMove(float DeltaTime)<br />
{<br />
local vector X,Y,Z;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> //GetAxes(Rotation,X,Y,Z);<br />
GetAxes(WorldInfo.Rotation,X,Y,Z);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> Pawn.Acceleration = PlayerInput.aForward*X + PlayerInput.aStrafe*Y + PlayerInput.aUp*vect(0,0,1);;<br />
Pawn.Acceleration = Pawn.AccelRate * Normal(Pawn.Acceleration);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if ( bCheatFlying && (Pawn.Acceleration == vect(0,0,0)) )<br />
Pawn.Velocity = vect(0,0,0);<br />
// Update rotation.<br />
UpdateRotation( DeltaTime );</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if ( Role < ROLE_Authority ) // then save this move and replicate it<br />
ReplicateMove(DeltaTime, Pawn.Acceleration, DCLICK_None, rot(0,0,0));<br />
else<br />
ProcessMove(DeltaTime, Pawn.Acceleration, DCLICK_None, rot(0,0,0));<br />
}<br />
}</span></blockquote>Finally, <em>replace</em> the DefaultProperties section. We dropped our CameraZOffice parameter and added a reference to a new camera object we're going to create:<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">DefaultProperties<br />
{<br />
//this makes us stop immediately instead of decelerating<br />
bCheatFlying=true<br />
<br />
InputClass=class'BattleMap.BattleMapPlayerInput'<br />
CameraClass=class'BattleMap.BattleMapCamera'<br />
}</span></blockquote><strong>BattleMapCamera.uc</strong><br />
<br />
This is a new class to handle the camera. The base PlayerController object doesn't include the same functionality as UTPlayerController, so we're moving our top-down camera code here.<br />
<br />
All we're doing is moving the camera position and POV after the real UpdateViewTarget "does its thing". We also want to force ApplyCameraModifiers() which handles things like camera shake. Because the camera will be so far from the source, the engine may try to skip that processing.<br />
<br />
We wrap our changes in a check to see if we're in CinematicMode. We don't want to change the camera if we are. This allows us to do things like fly-throughs of a big room before a boss fight:<br />
<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">class BattleMapCamera extends Camera;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">var int CameraZOffset;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// let it do it's fancy thing, then just set our own LOC and FOV<br />
function UpdateViewTarget(out TViewTarget OutVT, float DeltaTime)<br />
{<br />
local vector Loc, Pos;<br />
local rotator Rot;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> super.UpdateViewTarget(OutVT, DeltaTime);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if (!PCOwner.bCinematicMode)<br />
{<br />
Rot = rotator(vect(0,0,-1));<br />
Loc = PCOwner.Pawn.Location;<br />
Loc.Z = Loc.Z + CameraZOffset;<br />
Pos = Loc - Vector(Rot);<br />
OutVT.POV.Location = Pos;<br />
OutVT.POV.Rotation = Rot;<br />
OutVT.POV.FOV = DefaultFOV;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> ApplyCameraModifiers(DeltaTime, OutVT.POV);<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">DefaultProperties<br />
{<br />
CameraZOffset=6000<br />
DefaultFOV=35<br />
}</span></blockquote><strong>BattleMapPlayerInput.uc</strong><br />
<br />
Lastly, we just need to change the base object of our PlayerInput class:<br />
<blockquote style="border-bottom: black 1px solid; border-left: black 1px solid; border-right: black 1px solid; border-top: black 1px solid;"><span style="font-family: "Courier New", Courier, monospace;">class BattleMapPlayerInput extends PlayerInput within BattleMapPlayerController;<br />
<br />
...</span></blockquote>Now when you launch the game, the Necropolis level should start behaving more like a D&D dungeon map. Use the WASD keys to pan the map and Space or C to zoom in and out. Hit G to turn on the grid and resize it with the mouse wheel.<br />
<br />
(Note: If you zoom in using the C key and the camera suddenly shifts and "sticks" for a few seconds, what's happening is that your pawn is dying. You've hit the Kill-Z for the level, just as if your avatar had run off a cliff. The camera should reset in a few seconds. In our own levels, we'll set Kill-Z low enough to compensate for the camera height.)<br />
<br />
<a href="http://www.4shared.com/file/oM2-Z_X0/The_Grid.html">The original top-down UT mode source files</a><br />
<a href="http://www.4shared.com/file/QWZr_U2N/More_Dungeon_Less_UT.html">The new BattleMap mode source files</a>ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com6tag:blogger.com,1999:blog-2513175893262458465.post-180631356196483662011-03-08T19:28:00.000-08:002011-03-08T19:28:34.865-08:00D&D BattleMap Using the UDK - The GridBuilding on our <a href="http://zombpir8ninja.blogspot.com/2011/03/d-battlemap-using-udk-fixed-hud.html">previous post</a>, let's add the iconic D&D grid. This is significantly easier using the Flash UI. I'll summarize at the end what we had to do to get it to work using just the UDK.<br />
<br />
<strong>Flash</strong><br />
In Flash, we simply created the grid using the drawing tools. Vertical and horizontal lines, using a hairline stroke, spaced 20 units apart, covering the entire scene:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZ0aGEFViBmdqNe5PrSaimJBdWvJVvotSO4fyMGgjLM-WKgVM93HPHeKz21UDMIfviCQxOwulXGjYe2JR-wE9CeIAqENx5C7GPGEZkn70kvbyJ51rr-du9dSG_CxaRIVPS6817TZBqj2J_/s1600/Grid.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="224" q6="true" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZ0aGEFViBmdqNe5PrSaimJBdWvJVvotSO4fyMGgjLM-WKgVM93HPHeKz21UDMIfviCQxOwulXGjYe2JR-wE9CeIAqENx5C7GPGEZkn70kvbyJ51rr-du9dSG_CxaRIVPS6817TZBqj2J_/s320/Grid.png" width="320" /></a></div><br />
In the ActionScript of our Actions layer, we added a couple of functions that will be called by UnrealScript. The first will show/hide the grid, the second will scale it. We hide the grid initially to make positioning easier:<br />
<blockquote><span style="font-family: "Courier New", Courier, monospace;">function GridToggle()<br />
{<br />
GridInst._visible = !GridInst._visible;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">function GridZoom(Param1:Number)<br />
{<br />
GridInst._xscale = GridInst._xscale + Param1;<br />
GridInst._yscale = GridInst._xscale;<br />
GridInst._x = stage.width*.5 - GridInst._width*.5;<br />
GridInst._y = stage.height*.5 - GridInst._height*.5;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">GridInst._visible = false;</span></blockquote><br />
From now on, whenever you make a change to the HUD you <em>must</em>: <br />
<ol><li>Publish the movie</li>
<li>Open the UDK Editor and reimport the movie into your package using the Content Browser.</li>
<li>Save the package</li>
<li>Copy the package from the <span style="font-family: "Courier New", Courier, monospace;">UDKGame\Content\Misc</span> folder to the <span style="font-family: "Courier New", Courier, monospace;">UDKGame\CookedPC</span> folder.</li>
</ol>Since we haven't actually referenced the package from the BM-Necropolis.udk map that we're testing with, the Unreal Frontend doesn't include it when it Cooks. Believe me, I've lost a lot of time trying to figure out why my ActionScript functions weren't being called when I simply forgot step 4 above.<br />
<br />
<strong>BattleMapGfxHud.uc</strong><br />
Add two wrapper functions for the ActionScript functions above:<br />
<blockquote><span style="font-family: "Courier New", Courier, monospace;">function CallGridToggle()<br />
{<br />
ActionScriptVoid("GridToggle");<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">function CallGridZoom( float Param1 )<br />
{<br />
ActionScriptVoid("GridZoom");<br />
}</span></blockquote><br />
<strong>BattleMapPlayerController.uc</strong> <br />
All we need to do here is reference a new Input class that we're going to create to interpret keyboard/mouse commands. Add this to the <span style="font-family: "Courier New", Courier, monospace;">DefaultProperties</span> section:<br />
<blockquote><span style="font-family: "Courier New", Courier, monospace;"> InputClass=class'BattleMap.BattleMapPlayerInput'</span></blockquote><br />
<strong>BattleMapPlayerInput.uc</strong> <br />
This is a new class that will call our wrapper functions based on input events. I arbitrarily chose a delta of 5.0% when scaling the grid:<br />
<blockquote><span style="font-family: "Courier New", Courier, monospace;">class BattleMapPlayerInput extends UTPlayerInput within BattleMapPlayerController;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">var float GridZoomDelta;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">exec function BMGridToggle()<br />
{<br />
BattleMapHUD(myHUD).CrossHairMovie.CallGridToggle();<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">exec function BMGridZoomIn()<br />
{<br />
BattleMapHUD(myHUD).CrossHairMovie.CallGridZoom(-GridZoomDelta);<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">exec function BMGridZoomOut()<br />
{<br />
BattleMapHUD(myHUD).CrossHairMovie.CallGridZoom(GridZoomDelta);<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">DefaultProperties<br />
{<br />
GridZoomDelta = 5.0f<br />
}</span></blockquote><br />
I know. We seem to have a lot of functions just calling each other. Here's the flow: <br />
<br />
Input Event > PlayerInput > GFxMoviePlayer > ActionScript<br />
<br />
<strong>DefaultInput.ini</strong><br />
Lastly, we need to specify what keyboard/mouse events will trigger those actions. In UDKGame\Config\DefaultInput.ini, scroll all the way down past "Editor Bindings". Add a new section:<br />
<blockquote><span style="font-family: "Courier New", Courier, monospace;">;-------------------<br />
; BattleMap Bindings<br />
;-------------------<br />
.Bindings=(Name="BMGridToggle",Command="BMGridToggle")<br />
.Bindings=(Name="BMGridZoomIn",Command="BMGridZoomIn")<br />
.Bindings=(Name="BMGridZoomOut",Command="BMGridZoomOut")<br />
-Bindings=(Name="G",Command="GBA_SwitchToBestWeapon")<br />
-Bindings=(Name="MouseScrollUp",Command="GBA_PrevWeapon")<br />
-Bindings=(Name="MouseScrollDown",Command="GBA_NextWeapon")<br />
.Bindings=(Name="G",Command="BMGridToggle")<br />
.Bindings=(Name="MouseScrollUp",Command="BMGridZoomOut")<br />
.Bindings=(Name="MouseScrollDown",Command="BMGridZoomIn")</span></blockquote><br />
This is doing three things: <br />
<ul><li>Maps Input Binding commands with UnrealScript (PlayerInput) functions (I just happened to have named them the same)</li>
<li>Unbinds previously specified inputs from their default commands</li>
<li>Binds those inputs to our new commands</li>
</ul>Essentially, we use the "G" key to toggle the grid and the mouse wheel to scale the grid.<br />
<br />
The result is:<br />
<br />
<iframe allowfullscreen="" frameborder="0" height="390" src="http://www.youtube.com/embed/rb8WQd1uirk" title="YouTube video player" width="480"></iframe><br />
<br />
For reference, to implement this just using the UDK and UnrealScript, we had to:<br />
<ul><li>Paint a grid texture</li>
<li>Model a grid mesh (essentially, a 2D plane)</li>
<li>Create a BattleMapGridActor in UnrealScript which instantiated the grid mesh, applied the texture, and included several functions for moving ("scaling" by moving up and down, and positioning horizontally)</li>
<li>Include numerous functions in BattleMapPlayerInput for spawning, moving and snapping the grid</li>
</ul>It was a huge amount of code, compared to the few changes above. I wonder what else we can move from UnrealScript to Flash...ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com4tag:blogger.com,1999:blog-2513175893262458465.post-52805179586624474322011-03-03T19:08:00.000-08:002011-03-04T07:11:57.794-08:00D&D BattleMap Using the UDK - Fixed HUD!After a <em>very</em> long wait...<br />
<br />
<iframe allowfullscreen="" frameborder="0" height="390" src="http://www.youtube.com/embed/pYzOQFpGPYE" title="YouTube video player" width="480"></iframe><br />
<br />
...we finally have our UDK BattleMap fixed. It has been shackled to the August version of the UDK since the versions from September forward replaced the HUD with ScaleForm. That change broke the code that provided mouse coordinates. At long last, we have have it working again with a custom ScaleForm HUD. <a href="http://forums.epicgames.com/showthread.php?t=742353&page=4">This thread on epicgames.com</a> provided the answer. To prove it, here's an updated version of our very first video. Note the custom cursor provided by our friend at <a href="http://monsterlayer.com/">monsterlayer.com</a>.<br />
<br />
<iframe allowfullscreen="" frameborder="0" height="390" src="http://www.youtube.com/embed/fj0MJDWMVsg" title="YouTube video player" width="480"></iframe><br />
<br />
So while we <em>FINALLY</em> begin the process of documenting how we created our D&D Battlemap, I'll give you a teaser and show you how we did the mod above.<br />
<br />
But first, a couple of disclaimers:<br />
<ul><li>This isn't going to be a step-by-step. There are far too many well written UDK tutorials out there, so I would just be giving myself carpal tunnel syndrome. I'll explain the code, but you should be able to <a href="http://lmgtfy.com/">find walkthroughs</a> on installing the UDK, compiling scripts and launching a custom game.</li>
<li>We compiled bits of code from countless blogs, articles, and forum posts to get our code working. And, embarrassingly, we recorded very few sources. We have absolutely no intention of taking credit for anyone else's work. So, if you see a bit of your own code in here, a flaming accusatory email isn't necessary. Tell us it's yours and we'll be happy to give you full credit for it.</li>
<li>We were teaching ourselves UnrealScript for the first time as we were attempting this project. There are probably simpler and more effective ways of doing what we did. We're not experts, so don't point and laugh. Tell us how to do it better. We love constructive criticism.</li>
<li>I apologize for the way the code is formatted in the blog post. Hopefully, it'll be readable once you paste it into an editor. I'll try to find a better want to represent it for the following posts.</li>
</ul>So, that being said, how do you play UDK top down like Alien Swarm? First, you'll need <a href="https://www.adobe.com/cfusion/tdrc/index.cfm?product=flash&promoid=EBYEX">Flash CS5</a> and <a href="http://forecourse.com/tag/scaleform/">Allar's AWESOME Blog</a>. I didn't use his Foreclosure UI, but his tutorials for installing Flash and setting up ScaleForm were invaluable.<br />
<br />
<strong>Flash</strong><br />
This ActionScript does the magic that fixed our BattleMap:<br />
<br />
<blockquote><span style="font-family: "Courier New", Courier, monospace;">import flash.external.ExternalInterface;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">Mouse.hide();</span><br />
<span style="font-family: "Courier New", Courier, monospace;">startDrag("CursorInst", true);</span><br />
<span style="font-family: "Courier New", Courier, monospace;">var mouseListener:Object = new Object();</span><br />
<span style="font-family: "Courier New", Courier, monospace;">mouseListener.onMouseMove = function()<br />
{<br />
CursorInst._x = _root._xmouse;<br />
CursorInst._y = _root._ymouse;<br />
<br />
ExternalInterface.call( "ReceiveMouseCoords", _root._xmouse, _root._ymouse );<br />
<br />
updateAfterEvent();<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">Mouse.addListener(mouseListener);</span></blockquote><br />
Even though this is just a quick demo it's the foundation of our BattleMap, so all our UnrealScript code starts in \UDK\UDK-2011-02\Development\Src\BattleMap\Classes.<br />
<br />
<strong>BattleMapMain.uc</strong><br />
<br />
No explaination needed here. This sets up the other classes:<br />
<br />
<blockquote><span style="font-family: "Courier New", Courier, monospace;">class BattleMapMain extends UTGame;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">DefaultProperties</span><br />
<span style="font-family: "Courier New", Courier, monospace;">{</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> HUDType=class'BattleMap.BattleMapHUD'</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> PlayerControllerClass=class'BattleMap.BattleMapPlayerController'</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> DefaultPawnClass=class'BattleMap.BattleMapPawn'</span><br />
<span style="font-family: "Courier New", Courier, monospace;"><br />
</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> // don't wait for game start, spawn immediately</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> bDelayedStart=false</span><br />
<span style="font-family: "Courier New", Courier, monospace;">}</span></blockquote><br />
<strong>BattleMapPawn.uc</strong><br />
<br />
This creates our avatar in the game. We had to override the BecomeViewTarget() function to skip creating the first person meshes:<br />
<br />
<blockquote><span style="font-family: "Courier New", Courier, monospace;">class BattleMapPawn extends UTPawn<br />
config(Game);</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// we're skipping right over UTPawn.BecomeViewTarget down to Pawn.BecomeViewTarget<br />
// since UTPawn version sets first person meshes (arms, gun, etc)<br />
simulated event BecomeViewTarget( PlayerController PC )<br />
{<br />
if (PhysicsVolume != None)<br />
{<br />
PhysicsVolume.NotifyPawnBecameViewTarget(self, PC);<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> // if we don't normally replicate health, but will want to do so now to this client, force an update<br />
if (!bReplicateHealthToAll && WorldInfo.NetMode != NM_Client)<br />
{<br />
PC.ForceSingleNetUpdateFor(self);<br />
}<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// have to set bOwnerNoSee=false so that we can see our own pawn<br />
DefaultProperties<br />
{<br />
Begin Object Name=WPawnSkeletalMeshComponent<br />
bOwnerNoSee=false<br />
End Object<br />
}</span></blockquote><strong>BattleMapGfxHud.uc</strong><br />
<br />
The wrapper for our custom ScaleForm HUD and receives its mouse coordinates:<br />
<br />
<blockquote><span style="font-family: "Courier New", Courier, monospace;">class BattleMapGFxHud extends GFxMoviePlayer;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">var int MouseX;<br />
var int MouseY;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">function Initialize()<br />
{<br />
Start();<br />
Advance(0.0f);<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// This is called from Flash.<br />
function ReceiveMouseCoords( float x, float y )<br />
{<br />
MouseX = x;<br />
MouseY = y;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">DefaultProperties<br />
{<br />
bDisplayWithHudOff = false<br />
<br />
// Path to your package/flash file here.<br />
MovieInfo = SwfMovie'BattleMapHud.BMHud'<br />
}</span></blockquote><strong>BattleMapHUD.uc</strong><br />
<br />
Here we're instantiating the HUD, converting the mouse's 2D coordinates into 3D space and passing the vector on to our PlayerController:<br />
<blockquote><span style="font-family: "Courier New", Courier, monospace;">class BattleMapHUD extends UDKHUD;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">var BattleMapGfxHud CrosshairMovie;<br />
var class<BattleMapGfxHud> CrosshairMovieClass;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">simulated function PostBeginPlay()<br />
{<br />
super.PostBeginPlay();</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if (CrosshairMovie == none)<br />
{<br />
CrosshairMovie = new CrosshairMovieClass;<br />
CrosshairMovie.Initialize();<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> SizeX = 1024.0f;<br />
SizeY = 764.0f;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">singular event Destroyed()<br />
{<br />
if( CrosshairMovie != none )<br />
{<br />
CrosshairMovie.Close( true );<br />
CrosshairMovie = none;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> Destroy();<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">function vector2D GetMouseCoordinates()<br />
{<br />
local Vector2D mousePos;<br />
<br />
mousePos.X = CrosshairMovie.MouseX;<br />
mousePos.Y = CrosshairMovie.MouseY;<br />
<br />
return mousePos;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">event PostRender()<br />
{<br />
local BattleMapPlayerController bmPlayerController;<br />
local vector startTrace;<br />
local Vector endTrace;<br />
local Vector mouseOrigin;<br />
local Vector mouseDirection;<br />
local string StringMessage;<br />
local Vector pawnEyeLocation;<br />
local Rotator pawnEyeRotator;<br />
<br />
super.PostRender();</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> StringMessage = "";<br />
bmPlayerController = BattleMapPlayerController(PlayerOwner);<br />
bmPlayerController.MousePosition = GetMouseCoordinates();<br />
//Deproject the mouse from screen coordinate to world coordinate and store World Origin and Dir.<br />
Canvas.DeProject(bmPlayerController.MousePosition, mouseOrigin, mouseDirection);<br />
if (bmPlayerController.Pawn != none)<br />
{<br />
startTrace = bmPlayerController.Pawn.Location;<br />
startTrace.Z += bmPlayerController.CameraZOffset;<br />
endTrace = startTrace + (mouseDirection * 5000);<br />
Trace(mouseOrigin, mouseDirection, endTrace, startTrace, true);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> // laser sight from pawn to reticle<br />
bmPlayerController.Pawn.GetActorEyesViewPoint(pawnEyeLocation, pawnEyeRotator);<br />
Draw3DLine(pawnEyeLocation, mouseOrigin, RedColor);<br />
}<br />
bmPlayerController.SetMouseOrigin(mouseOrigin);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> StringMessage = "NetMode=" @ WorldInfo.NetMode @ "\nPlayerController=" @ PlayerOwner @ "\nControllerRotation=" @ bmPlayerController.Rotation @ "\nPawn=" @ PlayerOwner.Pawn @ "\nPawnRotation=" @ bmPlayerController.Pawn.Rotation @ "\nPawnEyeRotation=" @ pawnEyeRotator;<br />
//`Log(StringMessage);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> Canvas.DrawColor = GreenColor;<br />
Canvas.SetPos( 10, 10 );<br />
Canvas.DrawText( StringMessage, false, , , TextRenderInfo );<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">DefaultProperties<br />
{<br />
CrosshairMovieClass = class'BattleMapGfxHud'<br />
}</span></blockquote><strong>BattleMapPlayerController.uc</strong><br />
<br />
Finally, we move the camera to a top down position, convert the WASD keys to NWSE, and make the pawn aim for the cursor:<br />
<br />
<blockquote><span style="font-family: "Courier New", Courier, monospace;">class BattleMapPlayerController extends UTPlayerController<br />
config(Game);</span><br />
<span style="font-family: "Courier New", Courier, monospace;">var int CameraZOffset;<br />
var Vector2D MousePosition;<br />
var Vector MouseOrigin;</span><br />
<span style="font-family: "Courier New", Courier, monospace;">simulated function SetMouseOrigin(Vector NewMouseOrigin)<br />
{<br />
MouseOrigin = NewMouseOrigin;<br />
ServerMouseOrigin(NewMouseOrigin);<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">reliable server function ServerMouseOrigin(Vector NewMouseOrigin)<br />
{<br />
MouseOrigin = NewMouseOrigin;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// This basically locks the camera to the pawn's location, and adds CameraZOffset to the Z of the camera.<br />
simulated event GetPlayerViewPoint( out vector out_Location, out Rotator out_Rotation )<br />
{<br />
// this right here is just terrible. but after spending days trying to figure out why the server <br />
// thought the client was in 3rd person, but the client thought it was in 1st, I just hacked it.<br />
bBehindView = true;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> // let's just ignore all the fancy crap<br />
if(Pawn == None)<br />
super.GetPlayerViewPoint(out_Location, out_Rotation);<br />
else<br />
out_Location = Pawn.Location;<br />
out_Location.Z += CameraZOffset;<br />
out_Rotation = rotator(vect(0,0,-1));<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// Force WASD to be NWSE, so use world rotation instead of pawn rotation<br />
state PlayerWalking<br />
{<br />
function PlayerMove( float DeltaTime )<br />
{<br />
local vector X,Y,Z, NewAccel;<br />
local eDoubleClickDir DoubleClickMove;<br />
local rotator OldRotation;<br />
local bool bSaveJump;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if( Pawn == None )<br />
{<br />
GotoState('Dead');<br />
}<br />
else<br />
{<br />
// Changed this:<br />
//GetAxes(Pawn.Rotation,X,Y,Z);<br />
GetAxes(WorldInfo.Rotation,X,Y,Z);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> // Update acceleration.<br />
NewAccel = PlayerInput.aForward*X + PlayerInput.aStrafe*Y;<br />
NewAccel.Z = 0;<br />
NewAccel = Pawn.AccelRate * Normal(NewAccel);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> DoubleClickMove = PlayerInput.CheckForDoubleClickMove( DeltaTime/WorldInfo.TimeDilation );</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> // Update rotation.<br />
OldRotation = Rotation;<br />
UpdateRotation( DeltaTime );<br />
bDoubleJump = false;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if( bPressedJump && Pawn.CannotJumpNow() )<br />
{<br />
bSaveJump = true;<br />
bPressedJump = false;<br />
}<br />
else<br />
{<br />
bSaveJump = false;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if( Role < ROLE_Authority ) // then save this move and replicate it<br />
{<br />
ReplicateMove(DeltaTime, NewAccel, DoubleClickMove, OldRotation - Rotation);<br />
}<br />
else<br />
{<br />
ProcessMove(DeltaTime, NewAccel, DoubleClickMove, OldRotation - Rotation);<br />
}<br />
bPressedJump = bSaveJump;<br />
}<br />
}<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// This makes our weapons aim to the pawn's rotation, not the controller's rotation<br />
function Rotator GetAdjustedAimFor(Weapon W, vector StartFireLoc)<br />
{<br />
local Vector pawnEyeLocation;<br />
local Rotator pawnEyeRotator;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if(Pawn != None)<br />
{<br />
//return Pawn.Rotation;<br />
Pawn.GetActorEyesViewPoint(pawnEyeLocation, pawnEyeRotator);<br />
return Rotator(MouseOrigin - pawnEyeLocation);<br />
}<br />
else return Rotation;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// Turn the pawn toward the cursor<br />
function UpdateRotation( float DeltaTime )<br />
{<br />
local Rotator NewRotation, ViewRotation; //DeltaRot</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> ViewRotation = Rotation;<br />
if (Pawn!=none)<br />
{<br />
ViewRotation = Rotator(MouseOrigin - Pawn.Location + (Pawn.EyeHeight * vect(0,0,1)));<br />
Pawn.SetDesiredRotation(ViewRotation);<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> //// Calculate Delta to be applied on ViewRotation<br />
//DeltaRot.Yaw = PlayerInput.aTurn;<br />
//DeltaRot.Pitch = PlayerInput.aLookUp;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> //ProcessViewRotation( DeltaTime, ViewRotation, DeltaRot );<br />
//SetRotation(ViewRotation);<br />
SetRotation(ViewRotation);</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> ViewShake( deltaTime );</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> NewRotation = ViewRotation;<br />
NewRotation.Roll = Rotation.Roll;</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> if ( Pawn != None )<br />
Pawn.FaceRotation(NewRotation, deltatime);<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">// Make sure our custom HUD is used<br />
reliable client function ClientSetHUD(class<HUD> newHUDType)<br />
{<br />
if ( myHUD != None )<br />
{<br />
myHUD.Destroy();<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;"> //myHUD = (newHUDType != None) ? Spawn(newHUDType, self) : None;<br />
myHUD = (newHUDType != None) ? Spawn(class'BattleMap.BattleMapHUD', self) : None;<br />
}</span><br />
<span style="font-family: "Courier New", Courier, monospace;">DefaultProperties<br />
{<br />
CameraZOffset=480 <br />
}</span></blockquote><br />
Seems rather simple now. To get it running though, we need to make a few .INI changes. <br />
<br />
<strong>DefaultEngine.ini</strong><br />
<br />
Add the following parameters to the appropriate sections in the INI. Use whatever screen resolution is appropriate for you. The dimensions below is the native resolution of our projector:<br />
<br />
<blockquote><span style="font-family: "Courier New", Courier, monospace;">[UnrealEd.EditorEngine]<br />
+ModEditPackages=BattleMap</span><br />
<br />
<span style="font-family: "Courier New", Courier, monospace;">[SystemSettings]<br />
Fullscreen=True<br />
ResX=1680<br />
ResY=1050</span></blockquote><br />
<strong>DefaultGame.ini</strong> <br />
<br />
This ensures that our custom code is run when using "Play From Here" in the UDK Editor:<br />
<br />
<blockquote><span style="font-family: "Courier New", Courier, monospace;">[Engine.GameInfo]<br />
+DefaultMapPrefixes=(Prefix="BM",bUsesCommonPackage=FALSE,GameType="BattleMap.BattleMapMain")</span></blockquote><br />
Almost there. To get a quick map going, copy Content\Maps\VCTF-Necropolis.udk to Content\Maps\BM-Necropolis.udk.<br />
<br />
Now, compile the code and cook the assets with the Unreal Frontend. After it completes, you'll need to copy the package with your custom HUD (ours is Content\Misc\BattleMapHud.upk) to the CookedPC folder.<br />
<br />
And lastly, add the following to the Use Url box before launching:<br />
<br />
<blockquote><span style="font-family: "Courier New", Courier, monospace;">BM-Necropolis.udk?game=BattleMap.BattleMapMain?Listen=true</span></blockquote><br />
There you go! Hopefully everything went smoothly and you're now playing Unreal top down. The code works in multiplayer as well. The target I was shooting at above is another game client connected. Don't forget to try out the vehicles. (And if anyone creates a <a href="http://en.wikipedia.org/wiki/Car_wars">Car Wars</a>, <a href="http://en.wikipedia.org/wiki/Autoduel">AutoDuel</a> or <a href="http://en.wikipedia.org/wiki/Darkwind">Darkwind</a> clone, dibs on beta testing.)ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com4tag:blogger.com,1999:blog-2513175893262458465.post-35260395811143739752010-12-30T10:26:00.000-08:002010-12-30T10:59:23.561-08:00D&D BattleMap Using the UDK - The SetupSo, if you've seen this video you know we do actually use this to play <a href="http://wizards.com/dnd/">D&D</a>:<br />
<div style="border-bottom: medium none; border-left: medium none; border-right: medium none; border-top: medium none;"><br />
</div><div class="separator" style="border-bottom: medium none; border-left: medium none; border-right: medium none; border-top: medium none; clear: both; text-align: center;"><iframe allowfullscreen='allowfullscreen' webkitallowfullscreen='webkitallowfullscreen' mozallowfullscreen='mozallowfullscreen' width='320' height='266' src='https://www.youtube.com/embed/0xdXtDWcy3A?feature=player_embedded' frameborder='0'></iframe></div><div class="separator" style="clear: both; text-align: center;"><br />
</div><div align="left" class="separator" style="clear: both; text-align: center;"></div><div class="separator" style="clear: both; text-align: left;">But how do we get that <a href="http://www.udk.com/">UDK</a> map onto the table? First, it starts with an <a href="http://www.optomausa.com/Product_detail.asp?product_id=462">Optoma Pro350W</a> projector. </div><div class="separator" style="clear: both; text-align: center;"><br />
</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRZkyp8XYIzdJwJTEUlFAqq3n4FG8v3vNXnJIuwlveWtrNZeBJ0VKs3noKcWZ4_QbnEfiXVLE3839VLIOe60_XTPp-yJWrZa_XXxk_979LJEsNx8NyG0MkM155e4vwfkU1o_znPwiTZ8mi/s1600/projector.bmp" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="240" n4="true" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRZkyp8XYIzdJwJTEUlFAqq3n4FG8v3vNXnJIuwlveWtrNZeBJ0VKs3noKcWZ4_QbnEfiXVLE3839VLIOe60_XTPp-yJWrZa_XXxk_979LJEsNx8NyG0MkM155e4vwfkU1o_znPwiTZ8mi/s320/projector.bmp" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><br />
</div><div class="separator" style="clear: both; text-align: left;">My wife had one requirement when we started this project: I had to be able to completely break down the entire setup and put her dining room back to a presentable state. That meant no permanent fixtures. We explored various ways of positioning the projector on top of cabinets, building an overhanging stand for it or (gasp!) hanging it from the chandelier. Then, one of our brilliant players came up with the idea of the mirror -- that way we wouldn't be restricted to aiming the projector at the table. That's a cheap $5 Wal-mart kids' room mirror that I removed the framing and backing from. It's plastic, very light weight, and hung with binder clips and picture wire from white hooks. (My wife's first compromise about "no permanent fixtures".)</div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: left;">The projector itself sits on a heavy duty <a href="http://www.allsop.com/laptop-stands/redmond-adjustable-laptop-stand/">Redmond Laptop Stand</a>, so that we could adjust the angle if necessary. That sits on a decorative shelf. (My patient wife's second compromise.)</div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj1UBW4U4RRwYVikf8GF0GQ21J9lo3H06hS16lPhFdYKOqFMP_VWahwh_R6Qku7utBF59ExtcnFzbcGLdM3Qs6GImAT9cYElpgp46l21N2ejgURsGZz-QLhWqta8Yee2qtYQuGh-f9cxDPc/s1600/room.bmp" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" n4="true" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj1UBW4U4RRwYVikf8GF0GQ21J9lo3H06hS16lPhFdYKOqFMP_VWahwh_R6Qku7utBF59ExtcnFzbcGLdM3Qs6GImAT9cYElpgp46l21N2ejgURsGZz-QLhWqta8Yee2qtYQuGh-f9cxDPc/s320/room.bmp" width="240" /></a></div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: left;">And those are the only two modifications we made to the room. After D&D night, I can remove every trace of the battlemap setup, save for the decorative shelf and the camouflaged hooks.</div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: left;">Ok, I said "only two" and you're probably thinking, "what is that huge statue looming over the room?!" Yes, well, that's a statue from the old Wizards of the Coast brick-and-mortar stores that used to be in the malls in our area. When the stores were closing, I bought a set. That was very early in our marriage. (My advice to young lovers: buy all your mid-life crisis toys early, cause you're not getting them later when you want them...) So, that's been "grandfathered" in, and doesn't count.</div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjyoy22uhBub9paAGO298R2vQjj-1LjpJgyEARdiDqzmyjtchdFCPwaI91_mtr5KhfogoWorVPjASIw36_ALxh_ORutaL4Vr0sDa-RNvEB30jePxQ4bBKcQeHR9bZmKhICTl_FZqvVIqhLA/s1600/statue.bmp" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" n4="true" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjyoy22uhBub9paAGO298R2vQjj-1LjpJgyEARdiDqzmyjtchdFCPwaI91_mtr5KhfogoWorVPjASIw36_ALxh_ORutaL4Vr0sDa-RNvEB30jePxQ4bBKcQeHR9bZmKhICTl_FZqvVIqhLA/s320/statue.bmp" width="240" /></a></div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: left;">Makes a great conversation piece, though. And scares the pizza delivery guys. :)</div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: left;">Anyway, where we used to have a battlemat or paper maps, we now use a sheet of <a href="http://www.hancockfabrics.com/Vinyl-Leather-Sheet-White-Backing-Fabric-Front-Page_stcVVproductId49133797VVcatId537258VVviewprod.htm">bright white vinyl</a> from a fabric store to project the map onto.</div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtrdv3dQgv0FlRIcWEHUdmGd_D6VvWIQZeZ32kt7ZYLOjOnWR8M3NpeYDu4y32x-uE8QvJvLE-NDUyX2aNgkjUArCIwF7_2uyyuh1EZbTWvLDDyxv7TW0t6D2_pJpdvp6kuHUnacv7k0ez/s1600/table.bmp" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="240" n4="true" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtrdv3dQgv0FlRIcWEHUdmGd_D6VvWIQZeZ32kt7ZYLOjOnWR8M3NpeYDu4y32x-uE8QvJvLE-NDUyX2aNgkjUArCIwF7_2uyyuh1EZbTWvLDDyxv7TW0t6D2_pJpdvp6kuHUnacv7k0ez/s320/table.bmp" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><br />
</div><div class="separator" style="clear: both; text-align: left;">No grid marks, so we can position the map anyway/anywhere we want to. And it's tough as leather, so we don't worry about drink stains or die rolls on the table.</div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: left;">Ok, so we now have the projector setup but what's it connected to? You may notice that our DM's laptop is running Excel, not the UDK...</div><br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgE1B8gPVheL0mUgMxHI598vmdIZ4Cmz78QmDJo9Bi4EqDTvfAiQnsDyldGDomj80M9UTJhh4mY10dpWgfJyPhZQ77HHHeDwXXAXDuQOzzL7Ze-EoHKuS8r13pfTuxYgJ8OSRrWscPXVnI_/s1600/DM+view.bmp" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" n4="true" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgE1B8gPVheL0mUgMxHI598vmdIZ4Cmz78QmDJo9Bi4EqDTvfAiQnsDyldGDomj80M9UTJhh4mY10dpWgfJyPhZQ77HHHeDwXXAXDuQOzzL7Ze-EoHKuS8r13pfTuxYgJ8OSRrWscPXVnI_/s320/DM+view.bmp" width="240" /></a></div><div class="separator" style="clear: both; text-align: center;"><br />
</div><div class="separator" style="clear: both; text-align: left;">It's prohibitively more expensive to buy a gaming capable laptop than a productivity laptop. That is an inexpensive <a href="http://h10025.www1.hp.com/ewfrf/wc/product?product=4326707&lc=en&cc=us&dlc=en&lang=en&cc=us">HP G56</a> laptop. It runs the UDK at a laughable 2 fps. But even if it could run it, it's not practical: we use an Excel based combat tracker. Flipping projected screens between the UDK and Excel would kill the immersion and probably cause seizures in at least one of us. So, what's driving the projector?</div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjt4Zw_UNYiLg2u8SJOE8H6jcUPZIDTSDCuTqpKGLDVNOp3ILFeak3reR_9y8kcm6ASep09rZeDR6ujnSVQurQNlV0fV27YcdG2VEi_2llrMiV0_46OfHsd_YB5ys7kBISqP_KCLp-E31Dm/s1600/PC.bmp" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" n4="true" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjt4Zw_UNYiLg2u8SJOE8H6jcUPZIDTSDCuTqpKGLDVNOp3ILFeak3reR_9y8kcm6ASep09rZeDR6ujnSVQurQNlV0fV27YcdG2VEi_2llrMiV0_46OfHsd_YB5ys7kBISqP_KCLp-E31Dm/s320/PC.bmp" width="240" /></a></div><div class="separator" style="clear: both; text-align: center;"><br />
</div><div class="separator" style="clear: both; text-align: left;">My monster of a <a href="http://www.ibuypower.com/lobby.aspx">gaming rig</a> sporting dual <a href="http://www.nvidia.com/object/product_geforce_gtx_285_us.html">GTX-285s</a> sits in the very next room. All it took was:</div><ul><li><div class="separator" style="clear: both; text-align: left;">50 feet of VGA extension cables</div></li>
<li><div class="separator" style="clear: both; text-align: left;"><a href="http://www.inputdirector.com/">Input Director</a>, to remotely control my rig from the DM's laptop</div></li>
</ul><div class="separator" style="clear: both; text-align: left;">(Just a side note about Input Director: this is one of the most useful applications I have ever downloaded. If you have more than one computer turned on at any one time time, it'll save you time, desk space, and body strain. I love this thing. I used to think dual-boxing gamers were ambidextrous, ADD, narcissistic, power gamers. I've tried it using Input Director and it's shockingly easy.)</div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: left;">The last thing we did was replace the light switch in the room with a <a href="http://www.lowes.com/pd_239662-539-LG-603PH-WH_4294821967_4294937087_?productId=3189411&Ns=p_product_price|1&pl=1&currentURL=%2Fpl_Dimmers%2B_4294821967_4294937087_%3FNs%3Dp_product_price%7C1%26page%3D2">dimmer</a>. When building a map, it can be difficult to determine how bright to light it. Using a dimmer, we could adjust the light in the room without having to rebuild the map on the fly. (And it didn't count against my "no permanent fixtures" limitation, since it benefited the room during normal use.)</div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: left;">And that's all the hardware we needed. The room can be completely returned to normal after all the monsters have been slain. The next blog will start explaining the UDK code itself.</div><br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhyL7ZvzVdlCEgAB9F_f4f_RO-wVTIHBjqNiss3rSoBiMKOdwESIEx3GJeD15ZlOPxeZtjb9El4VbEXIbvoM1Nkh1Ugz6jnRtgL94sYqZb7H5EXIkacLBQUedUfvD_LeJQL0MppI-wohhYI/s1600/minis.bmp" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="240" n4="true" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhyL7ZvzVdlCEgAB9F_f4f_RO-wVTIHBjqNiss3rSoBiMKOdwESIEx3GJeD15ZlOPxeZtjb9El4VbEXIbvoM1Nkh1Ugz6jnRtgL94sYqZb7H5EXIkacLBQUedUfvD_LeJQL0MppI-wohhYI/s320/minis.bmp" width="320" /></a></div>ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com13tag:blogger.com,1999:blog-2513175893262458465.post-66728177630575022762010-12-30T08:58:00.000-08:002010-12-30T10:55:37.715-08:00D&D BattleMap Using the UDKIf you've come here from the <a href="http://www.youtube.com/user/ZomBPir8Ninja?feature=mhum">YouTube videos</a>, then you know what this is about. If not, let me give you some background. I'm sure many <a href="http://wizards.com/dnd/">D&D</a> DMs reach the same conclusion we did: battlemaps are difficult to make interesting.<br />
<ul><li><a href="http://www.chessex.com/mats/Battlemats_Megamats.htm">Wet erase vinyl maps</a> require some time and artistic talent to be more than just room-defining walls. Plus, you can't really prepare them beforehand if the evening's adventure will be in more than one location.</li>
<li>Scanning maps from a module, scaling, printing, cutting and taping uses up a lot of material that gets thrown away at the end of the evening.</li>
<li><a href="http://www.wizards.com/dnd/Product.aspx?x=dnd/products/dndacc/317130000">Dungeon tiles</a> are great if the adventure you are running used them. Otherwise, they can be clunky to try to fit together to match your design.</li>
</ul>Which left us with various digital map tools. But if we're sitting around the table with laptops staring at a networked map, we loose the immersion of using the miniatures. (More on that later.)<br />
<br />
It was during this period of battlemap exploration that we came across the <a href="http://www.udk.com/">UDK</a>. We're a group of IT industry professionals who play D&D. Surely, we could spend some of our gaming time creating our <em>own</em> game, right? But what game to make? Soon afterward, <a href="http://en.wikipedia.org/wiki/Alien_Swarm">Alien Swarm</a> entered the picture and we had our epiphany: if we made a top-down game, why couldn't we just overlay that with a grid and have a fully animated, realistic, 3D battlemap?<br />
<br />
Well, we did. And came up with this:<br />
<div class="separator" style="clear: both; text-align: center;"><iframe allowfullscreen='allowfullscreen' webkitallowfullscreen='webkitallowfullscreen' mozallowfullscreen='mozallowfullscreen' width='320' height='266' src='https://www.youtube.com/embed/9j4EQFP1C7w?feature=player_embedded' frameborder='0'></iframe></div><br />
The next blog will talk about our setup and how we actually use it to play. Then we'll explain the code we wrote to build it. Until then, check out some of the other videos. We're kinda proud of them. :)<br />
<br />
<a href="http://zombpir8ninja.blogspot.com/2010/12/d-battlemap-using-udk-setup.html">D&D BattleMap Using the UDK - The Setup</a>ZomBPir8Ninjahttp://www.blogger.com/profile/14079483519782292554noreply@blogger.com4