You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/AutomatedTesting/Gem/PythonTests/physics/collider/C4982797_Collider_ColliderO...

335 lines
14 KiB
Python

"""
Copyright (c) Contributors to the Open 3D Engine Project.
For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
# Test case ID : C4982797
# Test Case Title : Check that collision offsets trigger collision events,
# not entity transform locations
# fmt: off
class Tests:
enter_game_mode = ("Entered game mode", "Failed to enter game mode")
boxes_found = ("All boxes were validated", "Couldn't validate at least one box")
spheres_found = ("All spheres were validated", "Not all the spheres could be validated")
test_completed = ("The test completed", "The test timed out")
target_spheres_passed = ("All spheres intended to collide with Target Boxes DID", "At least one sphere intended to collide with a Target Box DID NOT")
pass_spheres_passed = ("All spheres intended to pass through Target Boxes DID", "At least one sphere intended to pass through a Target Box DID NOT")
exit_game_mode = ("Exited game mode", "Couldn't exit game mode")
# fmt: on
def C4982797_Collider_ColliderOffset():
# type: () -> None
"""
Summary:
Runs an automated test to ensure that PhysXCollider offsets work properly. Collisions should be
calculated based on the location of the colliders, not the geometry of the entity's Transform
Level Description:
There are five classes of entities in the level: Target Spheres, Pass Spheres, Target Boxes, Pass Boxes
and Fail Boxes. All entities have gravity disabled, are positioned above the terrain, and have the same
collision group/layer (Default/All).
Target Boxes: These are the actual boxes that have their colliders offset. There are three of these
boxes, one for each offset axis (rightly labeled Box_[X, Y, or Z]_Target).
Target Spheres: These spheres are positioned near the Target Boxes' collision offset areas. These entities are
initialized with a velocity that will send them on course to collide with the collision geometry for the
Target Boxes. They are appropriately labeled for which Target box they are to collide with
Sphere_[X, Y, or Z]_Target
Pass Spheres: The spheres are positioned near the Target Boxes as well, and are also initialized with a velocity
that will will send them towards their Target Box. The difference here is that they aligned to "collide"
with their Target Box's actual transform (not their collider offset). They too are appropriately named
Sphere_[X, Y, or Z]_Pass
Pass Boxes: Pass Boxes are positioned on the other side of the Pass Spheres from their target Box. They serve
as a trigger to register that the Pass Sphere successfully passed through the respective Target Box's
transform geometry. They are rightfully named Box_[X, Y, or Z]_Pass
Fail Boxes: Fail boxes (like Pass Boxes) are positioned on the other side of their respective Target Box,
but they are arranged opposite the Target Sphere (rather than the Pass Sphere). They act as a fail safe
if the Target Sphere happens the pass through the Target Box's collider offset. They are named following the
same convention: Box_[X, Y, or Z]_Fail
Note: All boxes are set to "kinematic" so they do not move when a collision happens.
Expected Behavior:
Upon entering game mode, the spheres should follow their initial velocities. The Target Spheres should collide and
bounce off of the Target Boxes' collision offsets, and the Pass Spheres should pass through the visible transforms
of their Target Boxes and collide with their respective Pass Boxes.
Test Steps:
1) Loads the level
2) Enters game mode
3) Retrieve and validate entities
4) Ensures that the test objects are located
5) Wait for test to complete or time out
6) Log the results
7) Exit game mode and editor
Note:
- This test file must be called from the Open 3D Engine Editor command terminal
- Any passed and failed tests are written to the Editor.log file.
Parsing the file or running a log_monitor are required to observe the test results.
:return: None
"""
# System imports
import os
import sys
# Internal editor imports
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
import azlmbr.legacy.general as general
import azlmbr.bus
import azlmbr
# ******** Global Variables ********
# Entity data organization class
class EntityData:
def __init__(self, name):
self.name = name
self.id = None
self.init_pos = None
self.current_pos = None
self.result = None
# fmt: off
# Establish entity sets
target_spheres = [EntityData("Sphere_X_Target"), EntityData("Sphere_Y_Target"), EntityData("Sphere_Z_Target")]
pass_spheres = [EntityData("Sphere_X_Pass"), EntityData("Sphere_Y_Pass"), EntityData("Sphere_Z_Pass")]
target_boxes = [EntityData("Box_X_Target"), EntityData("Box_Y_Target"), EntityData("Box_Z_Target")]
pass_boxes = [EntityData("Box_X_Pass"), EntityData("Box_Y_Pass"), EntityData("Box_Z_Pass")]
fail_boxes = [EntityData("Box_X_Fail"), EntityData("Box_Y_Fail"), EntityData("Box_Z_Fail")]
all_spheres = target_spheres + pass_spheres
all_boxes = pass_boxes + fail_boxes + target_boxes
all_entities = all_spheres + all_boxes
# Maps a Sphere to its last anticipated collision/position (by name)
final_expected_collisions = {
"Sphere_X_Target": "Box_X_Fail",
"Sphere_Y_Target": "Box_Y_Fail",
"Sphere_Z_Target": "Box_Z_Fail",
"Sphere_X_Pass": "Box_X_Pass",
"Sphere_Y_Pass": "Box_Y_Pass",
"Sphere_Z_Pass": "Box_Z_Pass",
}
# Possible results
PASS = "pass"
FAIL = "fail"
TARGET = "target"
# fmt: on
# ******** Helper Functions ********
# Validate entities' IDs and initial positions.
# Fast Fails if there are any problems retrieving vital information
def validate_entities(entity_list, test_tuple):
# type: ([EntityData], (str, str)) -> None
passed = True
for entity in entity_list:
valid = True
entity.id = general.find_game_entity(entity.name)
if not entity.id.IsValid():
valid = False
Report.info("Entity: {} could not be validated".format(entity.name))
entity.init_pos = entity.current_pos = azlmbr.components.TransformBus(
azlmbr.bus.Event, "GetWorldTranslation", entity.id
)
if entity.init_pos is None or entity.init_pos.IsZero():
valid = False
Report.info("Entity: {}'s initial position could not be found".format(entity.name))
if not valid:
passed = False
break
Report.critical_result(test_tuple, passed)
# Updates entities' current position
def update_positions(entity_list):
# type: ([EntityData]) -> None
for entity in entity_list:
entity.current_pos = azlmbr.components.TransformBus(azlmbr.bus.Event, "GetWorldTranslation", entity.id)
# Looks for implicit failure cases for moving spheres
# by checking if they have moved through their "final expected collision" object
def check_for_failure(sphere_entities):
# type: ([EntityData]) -> None
for sphere in sphere_entities:
expected_name = final_expected_collisions[sphere.name]
expected_entity_list = [entity for entity in all_entities if entity.name == expected_name]
if len(expected_entity_list) == 0:
# Just in case we can't find the entity's "final expected collision" entity
Report.info("Failed finding {} in expected entities list:".format(expected_name))
Report.info(" {}".format(expected_entity_list))
helper.fail_fast()
expected_entity = expected_entity_list[0]
if sphere.result != FAIL and has_passed_through(sphere, expected_entity):
sphere.result = FAIL
Report.info("{} has unexpectedly passed through {}".format(sphere.name, expected_name))
# Checks for unexpected movement in stationary objects
# Fast Fails and writes to the log if there is a difference between initial position and current position
def check_for_unexpected_movement(entity_list):
# type: ([EntityData]) -> None
for entity in entity_list:
if not entity.init_pos.IsClose(entity.current_pos, CLOSE_ENOUGH_THRESHOLD):
helper.fail_fast("{} has unexpectedly moved".format(entity.name))
# Verifies the results for the entities passed in.
# Returns a count of the verified results
def verify_results(entity_list, expected_result):
# type: ([EntityData], str) -> int
results_verified = 0
for entity in entity_list:
if entity.result == expected_result:
results_verified += 1
else:
Report.info("{} had unexpected result: {}".format(entity.name, entity.result))
return results_verified
# Batch assign event handlers
def set_handlers(entity_list, callback, event="OnCollisionBegin"):
# type: ([EntityData], function, str) -> [handler]
handlers = []
for entity in entity_list:
handler = azlmbr.physics.CollisionNotificationBusHandler()
handler.connect(entity.id)
handler.add_callback(event, callback)
handlers.append(handler)
return handlers
# Checks to see if we are done collecting results for the test
def done_collecting_results(entity_list, num_results):
# type: ([EntityData], int) -> bool
result_count = 0
for entity in entity_list:
if entity.result is not None:
result_count += 1
# When all spheres have a result we are done
return result_count == num_results
# Checks if a moving entity has passed through a stationary entity.
# There is an assumption that the stationary entity should not have substantial movement
def has_passed_through(moving_entity, stationary_entity):
# type: (EntityData, EntityData) -> bool
init_diff = stationary_entity.init_pos.Subtract(moving_entity.init_pos).Unary()
current_diff = stationary_entity.current_pos.Subtract(moving_entity.current_pos).Unary()
angle = init_diff.AngleSafeDeg(current_diff)
# Angle > 90 degrees represents a change of sides
result = angle > 90.0
return result
def test_completed():
update_positions(all_entities)
check_for_failure(all_spheres)
check_for_unexpected_movement(all_boxes)
return done_collecting_results(all_spheres, TOTAL_SPHERES)
# ******** Event Handlers ********
# General collision handler
def on_collision_begin(collider_id, result):
# type: (EntityId, str) -> None
for sphere in all_spheres:
if sphere.id.Equal(collider_id):
Report.info("Entity: {} collided with a {} box".format(sphere.name, result))
if result is FAIL or sphere.result is None:
# Set result if not set yet OR the result is a failure (failure overrides success)
sphere.result = result
return
# It wasn't a sphere that collided, something went wrong
if collider_id.IsValid():
entity_name = azlmbr.entity.GameEntityContextRequestBus(azlmbr.bus.Broadcast, "GetEntityName", collider_id)
Report.info("{} box collided with unexpected entity: {}".format(result, entity_name))
# Fail Box collision event handler
def on_collision_begin_fail_box(args):
# type: ([EntityId, ...]) -> None
on_collision_begin(args[0], FAIL)
# Success Box Collision Event Handler
def on_collision_begin_pass_box(args):
# type: ([EntityId, ...]) -> None
on_collision_begin(args[0], PASS)
# Target box collision event handler
def on_collision_begin_target_box(args):
# type: ([EntityId, ...]) -> None
on_collision_begin(args[0], TARGET)
# ******** Execution Code *********
# Local Constants
TIME_OUT = 2.0
CLOSE_ENOUGH_THRESHOLD = 0.1
TOTAL_TARGET_SPHERES = 3
TOTAL_PASS_SPHERES = 3
TOTAL_SPHERES = TOTAL_TARGET_SPHERES + TOTAL_PASS_SPHERES
helper.init_idle()
# 1) Open level
helper.open_level("Physics", "C4982797_Collider_ColliderOffset")
# 2) Enter game mode
helper.enter_game_mode(Tests.enter_game_mode)
# 3) Retrieve and validate entities
validate_entities(all_boxes, Tests.boxes_found)
validate_entities(all_spheres, Tests.spheres_found)
# Assign handlers
handlers = []
handlers = handlers + set_handlers(fail_boxes, on_collision_begin_fail_box)
handlers = handlers + set_handlers(pass_boxes, on_collision_begin_pass_box)
handlers = handlers + set_handlers(target_boxes, on_collision_begin_target_box)
# 4) Wait for either time out or for the test to complete
Report.result(Tests.test_completed, helper.wait_for_condition(test_completed, TIME_OUT))
# 5) Log results
# Verify entities results
pass_spheres_passed = verify_results(pass_spheres, PASS)
target_spheres_passed = verify_results(target_spheres, TARGET)
# Report results
Report.result(Tests.target_spheres_passed, target_spheres_passed == TOTAL_TARGET_SPHERES)
Report.result(Tests.pass_spheres_passed, pass_spheres_passed == TOTAL_PASS_SPHERES)
# Data dump at bottom of log
Report.info("******** Collected Data *********")
for entity in all_entities:
Report.info("Entity: {}".format(entity.name))
Report.info_vector3(entity.init_pos, " Initial position:")
Report.info_vector3(entity.current_pos, " Final position:")
Report.info(" Result: {}".format(entity.result))
Report.info("********************************")
# 6) Exit Game mode
helper.exit_game_mode(Tests.exit_game_mode)
Report.info("*** FINISHED TEST ***")
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(C4982797_Collider_ColliderOffset)